All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v6 0/5] landlock: truncate support
@ 2022-09-08 19:58 Günther Noack
  2022-09-08 19:58 ` [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook Günther Noack
                   ` (4 more replies)
  0 siblings, 5 replies; 39+ messages in thread
From: Günther Noack @ 2022-09-08 19:58 UTC (permalink / raw)
  To: linux-security-module
  Cc: Mickaël Salaün, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze,
	Günther Noack

The goal of these patches is to work towards a more complete coverage
of file system operations that are restrictable with Landlock.

The known set of currently unsupported file system operations in
Landlock is described at [1]. Out of the operations listed there,
truncate is the only one that modifies file contents, so these patches
should make it possible to prevent the direct modification of file
contents with Landlock.

The patch introduces the truncation restriction feature as an
additional bit in the access_mask_t bitmap, in line with the existing
supported operations.

The truncation flag covers both the truncate(2) and ftruncate(2)
families of syscalls, as well as open(2) with the O_TRUNC flag.
This includes usages of creat() in the case where existing regular
files are overwritten.

Apart from Landlock, file truncation can also be restricted using
seccomp-bpf, but it is more difficult to use (requires BPF, requires
keeping up-to-date syscall lists) and it is not configurable by file
hierarchy, as Landlock is. The simplicity and flexibility of the
Landlock approach makes it worthwhile adding.

While it's possible to use the "write file" and "truncate" rights
independent of each other, it simplifies the mental model for
userspace callers to always use them together.

Specifically, the following behaviours might be surprising for users
when using these independently:

 * The commonly creat() syscall requires the truncate right when
   overwriting existing files, as it is equivalent to open(2) with
   O_TRUNC|O_CREAT|O_WRONLY.
 * The "write file" right is not always required to truncate a file,
   even through the open(2) syscall (when using O_RDONLY|O_TRUNC).

Nevertheless, keeping the two flags separate is the correct approach
to guarantee backwards compatibility for existing Landlock users.

Notably, the availability of the truncate right is associated with an
opened file when opening the file and is later checked to authorize
ftruncate(2) operations. This is similar to how the write mode gets
remembered after a open(..., O_WRONLY) to authorize later write()
operations. These opened file descriptors can also be passed between
processes and will continue to enforce their truncation properties
when these processes attempt an ftruncate().

These patches are based on version 6.0-rc4.

Best regards,
Günther

[1] https://docs.kernel.org/userspace-api/landlock.html#filesystem-flags

Past discussions:
V1: https://lore.kernel.org/all/20220707200612.132705-1-gnoack3000@gmail.com/
V2: https://lore.kernel.org/all/20220712211405.14705-1-gnoack3000@gmail.com/
V3: https://lore.kernel.org/all/20220804193746.9161-1-gnoack3000@gmail.com/
V4: https://lore.kernel.org/all/20220814192603.7387-1-gnoack3000@gmail.com/
V5: https://lore.kernel.org/all/20220817203006.21769-1-gnoack3000@gmail.com/

Changelog:

V6:
* LSM hooks: create file_truncate hook in addition to path_truncate.
  Use it in the existing path_truncate call sites where appropriate.
* landlock: check LANDLOCK_ACCESS_FS_TRUNCATE right during open(), and
  associate that right with the opened struct file in a security blob.
  Introduce get_path_access_rights() helper function.
* selftests: test ftruncate in a separate test, to exercise that
  the rights are associated with the file descriptor.
* Documentation: Rework documentation to reflect new ftruncate() semantics.
* Applied small fixes by Mickaël Salaün which he added on top of V5, in
  https://git.kernel.org/pub/scm/linux/kernel/git/mic/linux.git/log/?h=next
  (I hope I found them all.)

V5:
* Documentation
  * Fix wording in userspace-api headers and in landlock.rst.
  * Move the truncation limitation section one to the bottom.
  * Move all .rst changes into the documentation commit.
* selftests
  * Remove _metadata argument from helpers where it became unnecessary.
  * Open writable file descriptors at the top of both tests, before Landlock
    is enabled, to exercise ftruncate() independently from open().
  * Simplify test_ftruncate and decouple it from exercising open().
  * test_creat(): Return errno on close() failure (it does not conflict).
  * Fix /* comment style */
  * Reorder blocks of EXPECT_EQ checks to be consistent within a test.
  * Add missing |O_TRUNC to a check in one test.
  * Put the truncate_unhandled test before the other.

V4:
 * Documentation
   * Clarify wording and syntax as discussed in review.
   * Use a less confusing error message in the example.
 * selftests:
   * Stop using ASSERT_EQ in test helpers, return EBADFD instead.
     (This is an intentionally uncommon error code, so that the source
     of the error is clear and the test can distinguish test setup
     failures from failures in the actual system call under test.)
 * samples/Documentation:
   * Use additional clarifying comments in the kernel backwards
     compatibility logic.

V3:
 * selftests:
   * Explicitly test ftruncate with readonly file descriptors
     (returns EINVAL).
   * Extract test_ftruncate, test_truncate, test_creat helpers,
     which simplified the previously mixed usage of EXPECT/ASSERT.
   * Test creat() behaviour as part of the big truncation test.
   * Stop testing the truncate64(2) and ftruncate64(2) syscalls.
     This simplifies the tests a bit. The kernel implementations are the
     same as for truncate(2) and ftruncate(2), so there is little benefit
     from testing them exhaustively. (We aren't testing all open(2)
     variants either.)
 * samples/landlock/sandboxer.c:
   * Use switch() to implement best effort mode.
 * Documentation:
   * Give more background on surprising truncation behaviour.
   * Use switch() in the example too, to stay in-line with the sample tool.
   * Small fixes in header file to address previous comments.
* misc:
  * Fix some typos and const usages.

V2:
 * Documentation: Mention the truncation flag where needed.
 * Documentation: Point out connection between truncation and file writing.
 * samples: Add file truncation to the landlock/sandboxer.c sample tool.
 * selftests: Exercise open(2) with O_TRUNC and creat(2) exhaustively.
 * selftests: Exercise truncation syscalls when the truncate right
   is not handled by Landlock.

Günther Noack (5):
  security: create file_truncate hook from path_truncate hook
  landlock: Support file truncation
  selftests/landlock: Selftests for file truncation support
  samples/landlock: Extend sample tool to support
    LANDLOCK_ACCESS_FS_TRUNCATE
  landlock: Document Landlock's file truncation support

 Documentation/userspace-api/landlock.rst     |  62 +++-
 fs/namei.c                                   |   6 +-
 fs/open.c                                    |   4 +-
 include/linux/lsm_hook_defs.h                |   1 +
 include/linux/security.h                     |   6 +
 include/uapi/linux/landlock.h                |  18 +-
 samples/landlock/sandboxer.c                 |  23 +-
 security/apparmor/lsm.c                      |   6 +
 security/landlock/fs.c                       |  88 +++++-
 security/landlock/fs.h                       |  18 ++
 security/landlock/limits.h                   |   2 +-
 security/landlock/setup.c                    |   1 +
 security/landlock/syscalls.c                 |   2 +-
 security/security.c                          |   5 +
 security/tomoyo/tomoyo.c                     |  13 +
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   | 287 ++++++++++++++++++-
 17 files changed, 508 insertions(+), 36 deletions(-)


base-commit: 7e18e42e4b280c85b76967a9106a13ca61c16179
--
2.37.3

^ permalink raw reply	[flat|nested] 39+ messages in thread

* [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook
  2022-09-08 19:58 [PATCH v6 0/5] landlock: truncate support Günther Noack
@ 2022-09-08 19:58 ` Günther Noack
  2022-09-08 20:09   ` Paul Moore
                     ` (3 more replies)
  2022-09-08 19:58 ` [PATCH v6 2/5] landlock: Support file truncation Günther Noack
                   ` (3 subsequent siblings)
  4 siblings, 4 replies; 39+ messages in thread
From: Günther Noack @ 2022-09-08 19:58 UTC (permalink / raw)
  To: linux-security-module
  Cc: Mickaël Salaün, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze,
	Günther Noack

Like path_truncate, the file_truncate hook also restricts file
truncation, but is called in the cases where truncation is attempted
on an already-opened file.

This is required in a subsequent commit to handle ftruncate()
operations differently to truncate() operations.

Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 fs/namei.c                    |  6 +++---
 fs/open.c                     |  4 ++--
 include/linux/lsm_hook_defs.h |  1 +
 include/linux/security.h      |  6 ++++++
 security/apparmor/lsm.c       |  6 ++++++
 security/security.c           |  5 +++++
 security/tomoyo/tomoyo.c      | 13 +++++++++++++
 7 files changed, 36 insertions(+), 5 deletions(-)

diff --git a/fs/namei.c b/fs/namei.c
index 53b4bc094db2..52105873d1f8 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -53,8 +53,8 @@
  * The new code replaces the old recursive symlink resolution with
  * an iterative one (in case of non-nested symlink chains).  It does
  * this with calls to <fs>_follow_link().
- * As a side effect, dir_namei(), _namei() and follow_link() are now 
- * replaced with a single function lookup_dentry() that can handle all 
+ * As a side effect, dir_namei(), _namei() and follow_link() are now
+ * replaced with a single function lookup_dentry() that can handle all
  * the special cases of the former code.
  *
  * With the new dcache, the pathname is stored at each inode, at least as
@@ -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 8a813fa5ca56..0831433e493a 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);
@@ -1271,7 +1271,7 @@ struct file *filp_open(const char *filename, int flags, umode_t mode)
 {
 	struct filename *name = getname_kernel(filename);
 	struct file *file = ERR_CAST(name);
-	
+
 	if (!IS_ERR(name)) {
 		file = file_open_name(name, flags, mode);
 		putname(name);
diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
index 60fff133c0b1..dee35ab253ba 100644
--- a/include/linux/lsm_hook_defs.h
+++ b/include/linux/lsm_hook_defs.h
@@ -177,6 +177,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/security.h b/include/linux/security.h
index 7bd0c490703d..f80b23382dd9 100644
--- a/include/linux/security.h
+++ b/include/linux/security.h
@@ -394,6 +394,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);
@@ -1011,6 +1012,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/security/apparmor/lsm.c b/security/apparmor/lsm.c
index e29cade7b662..98ecb7f221b8 100644
--- a/security/apparmor/lsm.c
+++ b/security/apparmor/lsm.c
@@ -329,6 +329,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)
 {
@@ -1232,6 +1237,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/security.c b/security/security.c
index 4b95de24bc8d..e491120c48ba 100644
--- a/security/security.c
+++ b/security/security.c
@@ -1210,6 +1210,11 @@ int security_path_truncate(const struct path *path)
 	return call_int_hook(path_truncate, 0, path);
 }
 
+int security_file_truncate(struct file *file)
+{
+	return call_int_hook(file_truncate, 0, file);
+}
+
 int security_path_chmod(const struct path *path, umode_t mode)
 {
 	if (unlikely(IS_PRIVATE(d_backing_inode(path->dentry))))
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
@@ -134,6 +134,18 @@ static int tomoyo_path_truncate(const struct path *path)
 	return tomoyo_path_perm(TOMOYO_TYPE_TRUNCATE, path, NULL);
 }
 
+/**
+ * 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().
  *
@@ -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),
-- 
2.37.3


^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v6 2/5] landlock: Support file truncation
  2022-09-08 19:58 [PATCH v6 0/5] landlock: truncate support Günther Noack
  2022-09-08 19:58 ` [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook Günther Noack
@ 2022-09-08 19:58 ` Günther Noack
  2022-09-09 13:51   ` Mickaël Salaün
  2022-09-12 19:41   ` Mickaël Salaün
  2022-09-08 19:58 ` [PATCH v6 3/5] selftests/landlock: Selftests for file truncation support Günther Noack
                   ` (2 subsequent siblings)
  4 siblings, 2 replies; 39+ messages in thread
From: Günther Noack @ 2022-09-08 19:58 UTC (permalink / raw)
  To: linux-security-module
  Cc: Mickaël Salaün, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze,
	Günther Noack

Introduce the LANDLOCK_ACCESS_FS_TRUNCATE flag for file truncation.

This flag hooks into the path_truncate LSM hook and covers file
truncation using truncate(2), ftruncate(2), open(2) with O_TRUNC, as
well as creat().

This change also increments the Landlock ABI version, updates
corresponding selftests, and updates code documentation to document
the flag.

The following operations are restricted:

open(): requires the LANDLOCK_ACCESS_FS_TRUNCATE right if a file gets
implicitly truncated as part of the open() (e.g. using O_TRUNC).

Notable special cases:
* open(..., O_RDONLY|O_TRUNC) can truncate files as well in Linux
* open() with O_TRUNC does *not* need the TRUNCATE right when it
  creates a new file.

truncate() (on a path): requires the LANDLOCK_ACCESS_FS_TRUNCATE
right.

ftruncate() (on a file): requires that the file had the TRUNCATE right
when it was previously opened.

Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 include/uapi/linux/landlock.h                | 18 ++--
 security/landlock/fs.c                       | 88 +++++++++++++++++++-
 security/landlock/fs.h                       | 18 ++++
 security/landlock/limits.h                   |  2 +-
 security/landlock/setup.c                    |  1 +
 security/landlock/syscalls.c                 |  2 +-
 tools/testing/selftests/landlock/base_test.c |  2 +-
 tools/testing/selftests/landlock/fs_test.c   |  7 +-
 8 files changed, 124 insertions(+), 14 deletions(-)

diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 23df4e0e8ace..8c0124c5cbe6 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -95,8 +95,16 @@ 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`. The right to truncate a file gets carried along with an opened
+ *   file descriptor for the purpose of :manpage:`ftruncate(2)`. 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 +147,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 +167,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/security/landlock/fs.c b/security/landlock/fs.c
index a9dbd99d9ee7..1b546edf69a6 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -146,7 +146,8 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
 #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 */
 
 /*
@@ -761,6 +762,47 @@ static bool collect_domain_accesses(
 	return ret;
 }
 
+/**
+ * get_path_access_rights - Returns the subset of rights in access_request
+ * which are permitted for the given path.
+ *
+ * @domain: The domain that defines the current restrictions.
+ * @path: The path to get access rights for.
+ * @access_request: The rights we are interested in.
+ *
+ * Returns: The access mask of the rights that are permitted on the given path,
+ * which are also a subset of access_request (to save some calculation time).
+ */
+static inline access_mask_t
+get_path_access_rights(const struct landlock_ruleset *const domain,
+		       const struct path *const path,
+		       access_mask_t access_request)
+{
+	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
+	unsigned long access_bit;
+	unsigned long access_req;
+
+	init_layer_masks(domain, access_request, &layer_masks);
+	if (!check_access_path_dual(domain, path, access_request, &layer_masks,
+				    NULL, 0, NULL, NULL)) {
+		/*
+		 * Return immediately for successful accesses and for cases
+		 * where everything is permitted because the path belongs to an
+		 * internal filesystem.
+		 */
+		return access_request;
+	}
+
+	access_req = access_request;
+	for_each_set_bit(access_bit, &access_req, ARRAY_SIZE(layer_masks)) {
+		if (layer_masks[access_bit]) {
+			/* If any layer vetoed the access right, remove it. */
+			access_request &= ~BIT_ULL(access_bit);
+		}
+	}
+	return access_request;
+}
+
 /**
  * current_check_refer_path - Check if a rename or link action is allowed
  *
@@ -1142,6 +1184,11 @@ 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)
@@ -1159,22 +1206,55 @@ static inline access_mask_t get_file_access(const struct file *const file)
 	/* __FMODE_EXEC is indeed part of f_flags, not f_mode. */
 	if (file->f_flags & __FMODE_EXEC)
 		access |= LANDLOCK_ACCESS_FS_EXECUTE;
+
 	return access;
 }
 
 static int hook_file_open(struct file *const file)
 {
+	access_mask_t access_req, access_rights;
+	const access_mask_t optional_rights = LANDLOCK_ACCESS_FS_TRUNCATE;
 	const struct landlock_ruleset *const dom =
 		landlock_get_current_domain();
 
-	if (!dom)
+	if (!dom) {
+		/* Grant all rights. */
+		landlock_file(file)->rights = LANDLOCK_MASK_ACCESS_FS;
 		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
 	 * evolution.
 	 */
-	return check_access_path(dom, &file->f_path, get_file_access(file));
+	access_req = get_file_access(file);
+	access_rights = get_path_access_rights(dom, &file->f_path,
+					       access_req | optional_rights);
+	if (access_req & ~access_rights)
+		return -EACCES;
+
+	/*
+	 * 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)->rights = access_rights;
+
+	return 0;
+}
+
+static int hook_file_truncate(struct file *const file)
+{
+	/*
+	 * We permit truncation if the truncation right was available at the
+	 * time of opening the file.
+	 */
+	if (!(landlock_file(file)->rights & LANDLOCK_ACCESS_FS_TRUNCATE))
+		return -EACCES;
+
+	return 0;
 }
 
 static struct security_hook_list landlock_hooks[] __lsm_ro_after_init = {
@@ -1194,6 +1274,8 @@ 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_truncate, hook_file_truncate),
 
 	LSM_HOOK_INIT(file_open, hook_file_open),
 };
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index 8db7acf9109b..275ba5375839 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -36,6 +36,18 @@ struct landlock_inode_security {
 	struct landlock_object __rcu *object;
 };
 
+/**
+ * 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 {
+	access_mask_t rights;
+};
+
 /**
  * struct landlock_superblock_security - Superblock security blob
  *
@@ -50,6 +62,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 735a0865ea11..f4d6fc7ed17f 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/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index da9290817866..72cdae277b02 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,
 					     LANDLOCK_CREATE_RULESET_VERSION));
 
 	ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 45de42a027c5..87b28d14a1aa 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -406,9 +406,10 @@ TEST_F_FORK(layout1, inval)
 #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)
 
-#define ACCESS_LAST LANDLOCK_ACCESS_FS_REFER
+#define ACCESS_LAST LANDLOCK_ACCESS_FS_TRUNCATE
 
 #define ACCESS_ALL ( \
 	ACCESS_FILE | \
@@ -422,7 +423,7 @@ TEST_F_FORK(layout1, inval)
 	LANDLOCK_ACCESS_FS_MAKE_FIFO | \
 	LANDLOCK_ACCESS_FS_MAKE_BLOCK | \
 	LANDLOCK_ACCESS_FS_MAKE_SYM | \
-	ACCESS_LAST)
+	LANDLOCK_ACCESS_FS_REFER)
 
 /* clang-format on */
 
-- 
2.37.3


^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v6 3/5] selftests/landlock: Selftests for file truncation support
  2022-09-08 19:58 [PATCH v6 0/5] landlock: truncate support Günther Noack
  2022-09-08 19:58 ` [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook Günther Noack
  2022-09-08 19:58 ` [PATCH v6 2/5] landlock: Support file truncation Günther Noack
@ 2022-09-08 19:58 ` Günther Noack
  2022-09-16 17:05   ` Mickaël Salaün
  2022-09-08 19:58 ` [PATCH v6 4/5] samples/landlock: Extend sample tool to support LANDLOCK_ACCESS_FS_TRUNCATE Günther Noack
  2022-09-08 19:58 ` [PATCH v6 5/5] landlock: Document Landlock's file truncation support Günther Noack
  4 siblings, 1 reply; 39+ messages in thread
From: Günther Noack @ 2022-09-08 19:58 UTC (permalink / raw)
  To: linux-security-module
  Cc: Mickaël Salaün, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze,
	Günther Noack

These tests exercise the following truncation operations:

* truncate() (truncate by path)
* ftruncate() (truncate by file descriptor)
* open with the O_TRUNC flag
* special case: creat(), which is open with O_CREAT|O_WRONLY|O_TRUNC.

in the following scenarios:

* Files with read, write and truncate rights.
* Files with read and truncate rights.
* Files with the truncate right.
* Files without the truncate right.

In particular, the following scenarios are enforced with the test:

* open() with O_TRUNC requires the truncate right, if it truncates a file.
  open() already checks security_path_truncate() in this case,
  and it required no additional check in the Landlock LSM's file_open hook.
* creat() requires the truncate right
  when called with an existing filename.
* creat() does *not* require the truncate right
  when it's creating a new file.
* ftruncate() requires that the file was opened by a thread that had
  the truncate right for the file at the time of open(). (The rights
  are carried along with the opened file.)

Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 tools/testing/selftests/landlock/fs_test.c | 280 +++++++++++++++++++++
 1 file changed, 280 insertions(+)

diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 87b28d14a1aa..ddc8c7e57e86 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -58,6 +58,7 @@ static const char file1_s2d3[] = TMP_DIR "/s2d1/s2d2/s2d3/f1";
 static const char file2_s2d3[] = TMP_DIR "/s2d1/s2d2/s2d3/f2";
 
 static const char dir_s3d1[] = TMP_DIR "/s3d1";
+static const char file1_s3d1[] = TMP_DIR "/s3d1/f1";
 /* dir_s3d2 is a mount point. */
 static const char dir_s3d2[] = TMP_DIR "/s3d1/s3d2";
 static const char dir_s3d3[] = TMP_DIR "/s3d1/s3d2/s3d3";
@@ -83,6 +84,7 @@ static const char dir_s3d3[] = TMP_DIR "/s3d1/s3d2/s3d3";
  * │           ├── f1
  * │           └── f2
  * └── s3d1
+ *     ├── f1
  *     └── s3d2
  *         └── s3d3
  */
@@ -208,6 +210,7 @@ static void create_layout1(struct __test_metadata *const _metadata)
 	create_file(_metadata, file1_s2d3);
 	create_file(_metadata, file2_s2d3);
 
+	create_file(_metadata, file1_s3d1);
 	create_directory(_metadata, dir_s3d2);
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_EQ(0, mount("tmp", dir_s3d2, "tmpfs", 0, "size=4m,mode=700"));
@@ -230,6 +233,7 @@ static void remove_layout1(struct __test_metadata *const _metadata)
 	EXPECT_EQ(0, remove_path(file1_s2d2));
 	EXPECT_EQ(0, remove_path(file1_s2d1));
 
+	EXPECT_EQ(0, remove_path(file1_s3d1));
 	EXPECT_EQ(0, remove_path(dir_s3d3));
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	umount(dir_s3d2);
@@ -3158,6 +3162,282 @@ TEST_F_FORK(layout1, proc_pipe)
 	ASSERT_EQ(0, close(pipe_fds[1]));
 }
 
+/* Invokes truncate(2) and returns its errno or 0. */
+static int test_truncate(const char *const path)
+{
+	if (truncate(path, 10) < 0)
+		return errno;
+	return 0;
+}
+
+/*
+ * Invokes creat(2) and returns its errno or 0.
+ * Closes the opened file descriptor on success.
+ */
+static int test_creat(const char *const path)
+{
+	int fd = creat(path, 0600);
+
+	if (fd < 0)
+		return errno;
+
+	/*
+	 * Mixing error codes from close(2) and creat(2) should not lead to any
+	 * (access type) confusion for this test.
+	 */
+	if (close(fd) < 0)
+		return errno;
+	return 0;
+}
+
+/*
+ * Exercises file truncation when it's not restricted,
+ * as it was the case before LANDLOCK_ACCESS_FS_TRUNCATE existed.
+ */
+TEST_F_FORK(layout1, truncate_unhandled)
+{
+	const char *const file_r = file1_s1d1;
+	const char *const file_w = file2_s1d1;
+	const char *const file_none = file1_s1d2;
+	const struct rule rules[] = {
+		{
+			.path = file_r,
+			.access = LANDLOCK_ACCESS_FS_READ_FILE,
+		},
+		{
+			.path = file_w,
+			.access = LANDLOCK_ACCESS_FS_WRITE_FILE,
+		},
+		/* Implicitly: No rights for file_none. */
+		{},
+	};
+
+	const __u64 handled = LANDLOCK_ACCESS_FS_READ_FILE |
+			      LANDLOCK_ACCESS_FS_WRITE_FILE;
+	int ruleset_fd;
+
+	/* Enable Landlock. */
+	ruleset_fd = create_ruleset(_metadata, handled, rules);
+
+	ASSERT_LE(0, ruleset_fd);
+	enforce_ruleset(_metadata, ruleset_fd);
+	ASSERT_EQ(0, close(ruleset_fd));
+
+	/*
+	 * Checks read right: truncate and open with O_TRUNC work, unless the
+	 * file is attempted to be opened for writing.
+	 */
+	EXPECT_EQ(0, test_truncate(file_r));
+	EXPECT_EQ(0, test_open(file_r, O_RDONLY | O_TRUNC));
+	EXPECT_EQ(EACCES, test_open(file_r, O_WRONLY | O_TRUNC));
+	EXPECT_EQ(EACCES, test_creat(file_r));
+
+	/*
+	 * Checks write right: truncate and open with O_TRUNC work, unless the
+	 * file is attempted to be opened for reading.
+	 */
+	EXPECT_EQ(0, test_truncate(file_w));
+	EXPECT_EQ(EACCES, test_open(file_w, O_RDONLY | O_TRUNC));
+	EXPECT_EQ(0, test_open(file_w, O_WRONLY | O_TRUNC));
+	EXPECT_EQ(0, test_creat(file_w));
+
+	/*
+	 * Checks "no rights" case: truncate works but all open attempts fail,
+	 * including creat.
+	 */
+	EXPECT_EQ(0, test_truncate(file_none));
+	EXPECT_EQ(EACCES, test_open(file_none, O_RDONLY | O_TRUNC));
+	EXPECT_EQ(EACCES, test_open(file_none, O_WRONLY | O_TRUNC));
+	EXPECT_EQ(EACCES, test_creat(file_none));
+}
+
+TEST_F_FORK(layout1, truncate)
+{
+	const char *const file_rwt = file1_s1d1;
+	const char *const file_rw = file2_s1d1;
+	const char *const file_rt = file1_s1d2;
+	const char *const file_t = file2_s1d2;
+	const char *const file_none = file1_s1d3;
+	const char *const dir_t = dir_s2d1;
+	const char *const file_in_dir_t = file1_s2d1;
+	const char *const dir_w = dir_s3d1;
+	const char *const file_in_dir_w = file1_s3d1;
+	int file_rwt_fd, file_rw_fd;
+	const struct rule rules[] = {
+		{
+			.path = file_rwt,
+			.access = LANDLOCK_ACCESS_FS_READ_FILE |
+				  LANDLOCK_ACCESS_FS_WRITE_FILE |
+				  LANDLOCK_ACCESS_FS_TRUNCATE,
+		},
+		{
+			.path = file_rw,
+			.access = LANDLOCK_ACCESS_FS_READ_FILE |
+				  LANDLOCK_ACCESS_FS_WRITE_FILE,
+		},
+		{
+			.path = file_rt,
+			.access = LANDLOCK_ACCESS_FS_READ_FILE |
+				  LANDLOCK_ACCESS_FS_TRUNCATE,
+		},
+		{
+			.path = file_t,
+			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
+		},
+		/* Implicitly: No access rights for file_none. */
+		{
+			.path = dir_t,
+			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
+		},
+		{
+			.path = dir_w,
+			.access = LANDLOCK_ACCESS_FS_WRITE_FILE,
+		},
+		{},
+	};
+	const __u64 handled = LANDLOCK_ACCESS_FS_READ_FILE |
+			      LANDLOCK_ACCESS_FS_WRITE_FILE |
+			      LANDLOCK_ACCESS_FS_TRUNCATE;
+	int ruleset_fd;
+
+	/* Enable Landlock. */
+	ruleset_fd = create_ruleset(_metadata, handled, rules);
+
+	ASSERT_LE(0, ruleset_fd);
+	enforce_ruleset(_metadata, ruleset_fd);
+	ASSERT_EQ(0, close(ruleset_fd));
+
+	/* Checks read, write and truncate rights: truncation works. */
+	EXPECT_EQ(0, test_truncate(file_rwt));
+	EXPECT_EQ(0, test_open(file_rwt, O_RDONLY | O_TRUNC));
+	EXPECT_EQ(0, test_open(file_rwt, O_WRONLY | O_TRUNC));
+
+	/* Checks read and write rights: no truncate variant works. */
+	EXPECT_EQ(EACCES, test_truncate(file_rw));
+	EXPECT_EQ(EACCES, test_open(file_rw, O_RDONLY | O_TRUNC));
+	EXPECT_EQ(EACCES, test_open(file_rw, O_WRONLY | O_TRUNC));
+
+	/*
+	 * Checks read and truncate rights: truncation works.
+	 *
+	 * Note: Files can get truncated using open() even with O_RDONLY.
+	 */
+	EXPECT_EQ(0, test_truncate(file_rt));
+	EXPECT_EQ(0, test_open(file_rt, O_RDONLY | O_TRUNC));
+	EXPECT_EQ(EACCES, test_open(file_rt, O_WRONLY | O_TRUNC));
+
+	/* Checks truncate right: truncate works, but can't open file. */
+	EXPECT_EQ(0, test_truncate(file_t));
+	EXPECT_EQ(EACCES, test_open(file_t, O_RDONLY | O_TRUNC));
+	EXPECT_EQ(EACCES, test_open(file_t, O_WRONLY | O_TRUNC));
+
+	/* Checks "no rights" case: No form of truncation works. */
+	EXPECT_EQ(EACCES, test_truncate(file_none));
+	EXPECT_EQ(EACCES, test_open(file_none, O_RDONLY | O_TRUNC));
+	EXPECT_EQ(EACCES, test_open(file_none, O_WRONLY | O_TRUNC));
+
+	/*
+	 * Checks truncate right on directory: truncate works on contained
+	 * files.
+	 */
+	EXPECT_EQ(0, test_truncate(file_in_dir_t));
+	EXPECT_EQ(EACCES, test_open(file_in_dir_t, O_RDONLY | O_TRUNC));
+	EXPECT_EQ(EACCES, test_open(file_in_dir_t, O_WRONLY | O_TRUNC));
+
+	/*
+	 * Checks creat in dir_w: This requires the truncate right when
+	 * overwriting an existing file, but does not require it when the file
+	 * is new.
+	 */
+	EXPECT_EQ(EACCES, test_creat(file_in_dir_w));
+
+	ASSERT_EQ(0, unlink(file_in_dir_w));
+	EXPECT_EQ(0, test_creat(file_in_dir_w));
+}
+
+static void landlock_single_path(struct __test_metadata *const _metadata,
+				 const char *const path, __u64 handled,
+				 __u64 permitted)
+{
+	const struct rule rules[] = {
+		{
+			.path = path,
+			.access = permitted,
+		},
+		{},
+	};
+	int ruleset_fd = create_ruleset(_metadata, handled, rules);
+
+	ASSERT_LE(0, ruleset_fd);
+
+	enforce_ruleset(_metadata, ruleset_fd);
+
+	ASSERT_EQ(0, close(ruleset_fd));
+}
+
+/* Invokes ftruncate(2) and returns its errno or 0. */
+static int test_ftruncate(int fd)
+{
+	if (ftruncate(fd, 10) < 0)
+		return errno;
+	return 0;
+}
+
+TEST_F_FORK(layout1, ftruncate)
+{
+	/*
+	 * This test opens a new file descriptor at different stages of
+	 * Landlock restriction:
+	 *
+	 * without restriction:                    ftruncate works
+	 * something else but truncate restricted: ftruncate works
+	 * truncate restricted and permitted:      ftruncate works
+	 * truncate restricted and not permitted:  ftruncate fails
+	 *
+	 * Whether this works or not is expected to depend on the time when the
+	 * FD was opened, not to depend on the time when ftruncate() was
+	 * called.
+	 */
+	const char *const path = file1_s1d1;
+	int fd0, fd1, fd2, fd3;
+
+	fd0 = open(path, O_WRONLY);
+	EXPECT_EQ(0, test_ftruncate(fd0));
+
+	landlock_single_path(_metadata, path,
+			     LANDLOCK_ACCESS_FS_READ_FILE |
+				     LANDLOCK_ACCESS_FS_WRITE_FILE,
+			     LANDLOCK_ACCESS_FS_WRITE_FILE);
+
+	fd1 = open(path, O_WRONLY);
+	EXPECT_EQ(0, test_ftruncate(fd0));
+	EXPECT_EQ(0, test_ftruncate(fd1));
+
+	landlock_single_path(_metadata, path, LANDLOCK_ACCESS_FS_TRUNCATE,
+			     LANDLOCK_ACCESS_FS_TRUNCATE);
+
+	fd2 = open(path, O_WRONLY);
+	EXPECT_EQ(0, test_ftruncate(fd0));
+	EXPECT_EQ(0, test_ftruncate(fd1));
+	EXPECT_EQ(0, test_ftruncate(fd2));
+
+	landlock_single_path(_metadata, path,
+			     LANDLOCK_ACCESS_FS_TRUNCATE |
+				     LANDLOCK_ACCESS_FS_WRITE_FILE,
+			     LANDLOCK_ACCESS_FS_WRITE_FILE);
+
+	fd3 = open(path, O_WRONLY);
+	EXPECT_EQ(0, test_ftruncate(fd0));
+	EXPECT_EQ(0, test_ftruncate(fd1));
+	EXPECT_EQ(0, test_ftruncate(fd2));
+	EXPECT_EQ(EACCES, test_ftruncate(fd3));
+
+	ASSERT_EQ(0, close(fd0));
+	ASSERT_EQ(0, close(fd1));
+	ASSERT_EQ(0, close(fd2));
+	ASSERT_EQ(0, close(fd3));
+}
+
 /* clang-format off */
 FIXTURE(layout1_bind) {};
 /* clang-format on */
-- 
2.37.3


^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v6 4/5] samples/landlock: Extend sample tool to support LANDLOCK_ACCESS_FS_TRUNCATE
  2022-09-08 19:58 [PATCH v6 0/5] landlock: truncate support Günther Noack
                   ` (2 preceding siblings ...)
  2022-09-08 19:58 ` [PATCH v6 3/5] selftests/landlock: Selftests for file truncation support Günther Noack
@ 2022-09-08 19:58 ` Günther Noack
  2022-09-12 19:05   ` Mickaël Salaün
  2022-09-08 19:58 ` [PATCH v6 5/5] landlock: Document Landlock's file truncation support Günther Noack
  4 siblings, 1 reply; 39+ messages in thread
From: Günther Noack @ 2022-09-08 19:58 UTC (permalink / raw)
  To: linux-security-module
  Cc: Mickaël Salaün, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze,
	Günther Noack

Update the sandboxer sample to restrict truncate actions. This is
automatically enabled by default if the running kernel supports
LANDLOCK_ACCESS_FS_TRUNCATE, expect for the paths listed in the
LL_FS_RW environment variable.

Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 samples/landlock/sandboxer.c | 23 ++++++++++++++---------
 1 file changed, 14 insertions(+), 9 deletions(-)

diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index 3e404e51ec64..771b6b10d519 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,10 +161,8 @@ static int populate_ruleset(const char *const env_var, const int ruleset_fd,
 	LANDLOCK_ACCESS_FS_MAKE_FIFO | \
 	LANDLOCK_ACCESS_FS_MAKE_BLOCK | \
 	LANDLOCK_ACCESS_FS_MAKE_SYM | \
-	LANDLOCK_ACCESS_FS_REFER)
-
-#define ACCESS_ABI_2 ( \
-	LANDLOCK_ACCESS_FS_REFER)
+	LANDLOCK_ACCESS_FS_REFER | \
+	LANDLOCK_ACCESS_FS_TRUNCATE)
 
 /* clang-format on */
 
@@ -226,11 +225,17 @@ int main(const int argc, char *const argv[], char *const *const envp)
 		return 1;
 	}
 	/* Best-effort security. */
-	if (abi < 2) {
-		ruleset_attr.handled_access_fs &= ~ACCESS_ABI_2;
-		access_fs_ro &= ~ACCESS_ABI_2;
-		access_fs_rw &= ~ACCESS_ABI_2;
+	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;
 	}
+	access_fs_ro &= ruleset_attr.handled_access_fs;
+	access_fs_rw &= ruleset_attr.handled_access_fs;
 
 	ruleset_fd =
 		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
-- 
2.37.3


^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH v6 5/5] landlock: Document Landlock's file truncation support
  2022-09-08 19:58 [PATCH v6 0/5] landlock: truncate support Günther Noack
                   ` (3 preceding siblings ...)
  2022-09-08 19:58 ` [PATCH v6 4/5] samples/landlock: Extend sample tool to support LANDLOCK_ACCESS_FS_TRUNCATE Günther Noack
@ 2022-09-08 19:58 ` Günther Noack
  2022-09-09 13:51   ` Mickaël Salaün
  2022-09-12 19:15   ` Mickaël Salaün
  4 siblings, 2 replies; 39+ messages in thread
From: Günther Noack @ 2022-09-08 19:58 UTC (permalink / raw)
  To: linux-security-module
  Cc: Mickaël Salaün, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze,
	Günther Noack

Use the LANDLOCK_ACCESS_FS_TRUNCATE flag in the tutorial.

Adapt the backwards compatibility example and discussion to remove the
truncation flag where needed.

Point out potential surprising behaviour related to truncate.

Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 Documentation/userspace-api/landlock.rst | 62 +++++++++++++++++++++---
 1 file changed, 54 insertions(+), 8 deletions(-)

diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index b8ea59493964..57802fd1e09b 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: May 2022
+:Date: September 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,26 @@ 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) {
-        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
+    switch (abi) {
+    case -1:
+            perror("The running kernel does not enable to use Landlock");
+            return 1;
+    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 +138,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
@@ -251,6 +262,32 @@ 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)`.  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 don't have an enforced Landlock ruleset.
+
 Compatibility
 =============
 
@@ -397,6 +434,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
-- 
2.37.3


^ permalink raw reply related	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook
  2022-09-08 19:58 ` [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook Günther Noack
@ 2022-09-08 20:09   ` Paul Moore
  2022-09-08 20:50     ` Günther Noack
  2022-09-08 20:28   ` Günther Noack
                     ` (2 subsequent siblings)
  3 siblings, 1 reply; 39+ messages in thread
From: Paul Moore @ 2022-09-08 20:09 UTC (permalink / raw)
  To: Günther Noack
  Cc: linux-security-module, Mickaël Salaün, James Morris,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Thu, Sep 8, 2022 at 3:58 PM Günther Noack <gnoack3000@gmail.com> wrote:
>
> Like path_truncate, the file_truncate hook also restricts file
> truncation, but is called in the cases where truncation is attempted
> on an already-opened file.
>
> This is required in a subsequent commit to handle ftruncate()
> operations differently to truncate() operations.
>
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
>  fs/namei.c                    |  6 +++---
>  fs/open.c                     |  4 ++--
>  include/linux/lsm_hook_defs.h |  1 +
>  include/linux/security.h      |  6 ++++++
>  security/apparmor/lsm.c       |  6 ++++++
>  security/security.c           |  5 +++++
>  security/tomoyo/tomoyo.c      | 13 +++++++++++++
>  7 files changed, 36 insertions(+), 5 deletions(-)

We need to get John and Tetsuo's ACKs on this patch, but in addition
to that I have two small comments (below).

> diff --git a/fs/namei.c b/fs/namei.c
> index 53b4bc094db2..52105873d1f8 100644
> --- a/fs/namei.c
> +++ b/fs/namei.c
> @@ -53,8 +53,8 @@
>   * The new code replaces the old recursive symlink resolution with
>   * an iterative one (in case of non-nested symlink chains).  It does
>   * this with calls to <fs>_follow_link().
> - * As a side effect, dir_namei(), _namei() and follow_link() are now
> - * replaced with a single function lookup_dentry() that can handle all
> + * As a side effect, dir_namei(), _namei() and follow_link() are now
> + * replaced with a single function lookup_dentry() that can handle all

Since this patch(set) is likely to go in via the Landlock tree, it is
very important to keep changes outside of security/landlock to a bare
minimum of what is required both to reduce merge conflicts and
highlight the significant changes.  This change doesn't appear
necessary ... and I'm having a hard time spotting the difference in
the lines.

> diff --git a/fs/open.c b/fs/open.c
> index 8a813fa5ca56..0831433e493a 100644
> --- a/fs/open.c
> +++ b/fs/open.c
> @@ -1271,7 +1271,7 @@ struct file *filp_open(const char *filename, int flags, umode_t mode)
>  {
>         struct filename *name = getname_kernel(filename);
>         struct file *file = ERR_CAST(name);
> -
> +

See my comment above about unnecessary changes.

-- 
paul-moore.com

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook
  2022-09-08 19:58 ` [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook Günther Noack
  2022-09-08 20:09   ` Paul Moore
@ 2022-09-08 20:28   ` Günther Noack
  2022-09-16 17:30     ` Mickaël Salaün
  2022-09-09  3:37   ` John Johansen
  2022-09-09 13:50   ` Mickaël Salaün
  3 siblings, 1 reply; 39+ messages in thread
From: Günther Noack @ 2022-09-08 20:28 UTC (permalink / raw)
  To: linux-security-module
  Cc: Mickaël Salaün, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze,
	Namjae Jeon, David Howells

Adding Namjae Jeon and David Howells as authors of the respective
files in fs/ksmbd and fs/cachefiles -- do you happen to know whether
these vfs_truncate() calls are using 'struct file's that are opened by
normal userspace processes, where LSM policies may apply?

P.S. In this patch I have looked for all places where the
security_path_truncate() hook was called, to see which of these should
rather use security_file_truncate() (and I made sure that it does the
same thing for all the LSMs that use it).

I'm confident that this does the right thing when truncate() or
ftruncate() are called from userspace, but one of the places that
still calls the path-based hook is vfs_truncate(), and this is called
from more places in the kernel than just from userspace:

init/initramfs.c
387:				vfs_truncate(&wfile->f_path, body_len);

security/keys/big_key.c
172:		vfs_truncate(&payload->path, 0);

fs/cachefiles/interface.c
242:		ret = vfs_truncate(&file->f_path, dio_size);

fs/cachefiles/namei.c
497:			ret = vfs_truncate(&path, ni_size);

fs/ksmbd/smb2pdu.c
2350:	int rc = vfs_truncate(path, 0);

fs/ksmbd/vfs.c
874:	err = vfs_truncate(&filp->f_path, size);

I suspect that these are benign but am not familiar with all of these
corners of the codebase. -- The question is: Some of these call
vfs_truncate() on the f_path of an existing struct file -- should
these rather be calling the security_file_truncate() than the
security_path_truncate() hook to authorize the truncation?

Specifically, I think:

* initramfs happens at system startup and LSMs should not interfere at
  this point yet
* security/keys does not use an opened struct file, so calling the
  path-based hook through vfs_truncate() is correct
* fs/cachefiles and fs/ksmbd use the file system from the kernel to
  expose it as another file system (in a cached form for cachefiles,
  and over the network for ksmbd). I suspect that these file systems
  are not handling 'struct file's which are opened in contexts where a
  LSM applies? It that a reasonable assumption?

Thanks,
Günther

On Thu, Sep 08, 2022 at 09:58:01PM +0200, Günther Noack wrote:
> Like path_truncate, the file_truncate hook also restricts file
> truncation, but is called in the cases where truncation is attempted
> on an already-opened file.
>
> This is required in a subsequent commit to handle ftruncate()
> operations differently to truncate() operations.
>
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
>  fs/namei.c                    |  6 +++---
>  fs/open.c                     |  4 ++--
>  include/linux/lsm_hook_defs.h |  1 +
>  include/linux/security.h      |  6 ++++++
>  security/apparmor/lsm.c       |  6 ++++++
>  security/security.c           |  5 +++++
>  security/tomoyo/tomoyo.c      | 13 +++++++++++++
>  7 files changed, 36 insertions(+), 5 deletions(-)
>
> diff --git a/fs/namei.c b/fs/namei.c
> index 53b4bc094db2..52105873d1f8 100644
> --- a/fs/namei.c
> +++ b/fs/namei.c
> @@ -53,8 +53,8 @@
>   * The new code replaces the old recursive symlink resolution with
>   * an iterative one (in case of non-nested symlink chains).  It does
>   * this with calls to <fs>_follow_link().
> - * As a side effect, dir_namei(), _namei() and follow_link() are now
> - * replaced with a single function lookup_dentry() that can handle all
> + * As a side effect, dir_namei(), _namei() and follow_link() are now
> + * replaced with a single function lookup_dentry() that can handle all
>   * the special cases of the former code.
>   *
>   * With the new dcache, the pathname is stored at each inode, at least as
> @@ -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 8a813fa5ca56..0831433e493a 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);
> @@ -1271,7 +1271,7 @@ struct file *filp_open(const char *filename, int flags, umode_t mode)
>  {
>  	struct filename *name = getname_kernel(filename);
>  	struct file *file = ERR_CAST(name);
> -
> +
>  	if (!IS_ERR(name)) {
>  		file = file_open_name(name, flags, mode);
>  		putname(name);
> diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
> index 60fff133c0b1..dee35ab253ba 100644
> --- a/include/linux/lsm_hook_defs.h
> +++ b/include/linux/lsm_hook_defs.h
> @@ -177,6 +177,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/security.h b/include/linux/security.h
> index 7bd0c490703d..f80b23382dd9 100644
> --- a/include/linux/security.h
> +++ b/include/linux/security.h
> @@ -394,6 +394,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);
> @@ -1011,6 +1012,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/security/apparmor/lsm.c b/security/apparmor/lsm.c
> index e29cade7b662..98ecb7f221b8 100644
> --- a/security/apparmor/lsm.c
> +++ b/security/apparmor/lsm.c
> @@ -329,6 +329,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)
>  {
> @@ -1232,6 +1237,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/security.c b/security/security.c
> index 4b95de24bc8d..e491120c48ba 100644
> --- a/security/security.c
> +++ b/security/security.c
> @@ -1210,6 +1210,11 @@ int security_path_truncate(const struct path *path)
>  	return call_int_hook(path_truncate, 0, path);
>  }
>
> +int security_file_truncate(struct file *file)
> +{
> +	return call_int_hook(file_truncate, 0, file);
> +}
> +
>  int security_path_chmod(const struct path *path, umode_t mode)
>  {
>  	if (unlikely(IS_PRIVATE(d_backing_inode(path->dentry))))
> 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
> @@ -134,6 +134,18 @@ static int tomoyo_path_truncate(const struct path *path)
>  	return tomoyo_path_perm(TOMOYO_TYPE_TRUNCATE, path, NULL);
>  }
>
> +/**
> + * 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().
>   *
> @@ -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),
> --
> 2.37.3
>

--

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook
  2022-09-08 20:09   ` Paul Moore
@ 2022-09-08 20:50     ` Günther Noack
  2022-09-08 22:04       ` Tetsuo Handa
  0 siblings, 1 reply; 39+ messages in thread
From: Günther Noack @ 2022-09-08 20:50 UTC (permalink / raw)
  To: Paul Moore
  Cc: linux-security-module, Mickaël Salaün, James Morris,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze,
	John Johansen, Tetsuo Handa

On Thu, Sep 08, 2022 at 04:09:06PM -0400, Paul Moore wrote:
> On Thu, Sep 8, 2022 at 3:58 PM Günther Noack <gnoack3000@gmail.com> wrote:
> >
> > Like path_truncate, the file_truncate hook also restricts file
> > truncation, but is called in the cases where truncation is attempted
> > on an already-opened file.
> >
> > This is required in a subsequent commit to handle ftruncate()
> > operations differently to truncate() operations.
> >
> > Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> > ---
> >  fs/namei.c                    |  6 +++---
> >  fs/open.c                     |  4 ++--
> >  include/linux/lsm_hook_defs.h |  1 +
> >  include/linux/security.h      |  6 ++++++
> >  security/apparmor/lsm.c       |  6 ++++++
> >  security/security.c           |  5 +++++
> >  security/tomoyo/tomoyo.c      | 13 +++++++++++++
> >  7 files changed, 36 insertions(+), 5 deletions(-)
>
> We need to get John and Tetsuo's ACKs on this patch, but in addition
> to that I have two small comments (below).

+CC: John Johansen and Tetsuo Handa -- this change is splitting up the
path_truncate LSM hook into a path_truncate and file_truncate variant,
one operating on the path as before, and one operating on a struct
file*. As a result, AppArmor and TOMOYO need to implement the
file-based hook as well and treat it the same as before by looking at
the file's ->f_path. Does this change seem reasonable to you?


> > diff --git a/fs/namei.c b/fs/namei.c
> > index 53b4bc094db2..52105873d1f8 100644
> > --- a/fs/namei.c
> > +++ b/fs/namei.c
> > @@ -53,8 +53,8 @@
> >   * The new code replaces the old recursive symlink resolution with
> >   * an iterative one (in case of non-nested symlink chains).  It does
> >   * this with calls to <fs>_follow_link().
> > - * As a side effect, dir_namei(), _namei() and follow_link() are now
> > - * replaced with a single function lookup_dentry() that can handle all
> > + * As a side effect, dir_namei(), _namei() and follow_link() are now
> > + * replaced with a single function lookup_dentry() that can handle all
>
> Since this patch(set) is likely to go in via the Landlock tree, it is
> very important to keep changes outside of security/landlock to a bare
> minimum of what is required both to reduce merge conflicts and
> highlight the significant changes.  This change doesn't appear
> necessary ... and I'm having a hard time spotting the difference in
> the lines.

Thanks for pointing this out, I fully agree!

Both edits that you flagged were unintentional, I should have double
checked this patch better. I'll address them in the next version.

(You can't see the difference here because my editor trimmed trailing
whitespace on save (*facepalm*) - I turned this off now.)


> > diff --git a/fs/open.c b/fs/open.c
> > index 8a813fa5ca56..0831433e493a 100644
> > --- a/fs/open.c
> > +++ b/fs/open.c
> > @@ -1271,7 +1271,7 @@ struct file *filp_open(const char *filename, int flags, umode_t mode)
> >  {
> >         struct filename *name = getname_kernel(filename);
> >         struct file *file = ERR_CAST(name);
> > -
> > +
>
> See my comment above about unnecessary changes.

Same here, I'm fixing that for the next version.

Thanks for the review,
Günther

--

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook
  2022-09-08 20:50     ` Günther Noack
@ 2022-09-08 22:04       ` Tetsuo Handa
  0 siblings, 0 replies; 39+ messages in thread
From: Tetsuo Handa @ 2022-09-08 22:04 UTC (permalink / raw)
  To: Günther Noack, Paul Moore
  Cc: linux-security-module, Mickaël Salaün, James Morris,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze,
	John Johansen

On 2022/09/09 5:50, Günther Noack wrote:
> On Thu, Sep 08, 2022 at 04:09:06PM -0400, Paul Moore wrote:
>> On Thu, Sep 8, 2022 at 3:58 PM Günther Noack <gnoack3000@gmail.com> wrote:
>>>
>>> Like path_truncate, the file_truncate hook also restricts file
>>> truncation, but is called in the cases where truncation is attempted
>>> on an already-opened file.
>>>
>>> This is required in a subsequent commit to handle ftruncate()
>>> operations differently to truncate() operations.
>>>
>>> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
>>
>> We need to get John and Tetsuo's ACKs on this patch, but in addition
>> to that I have two small comments (below).
> 
> +CC: John Johansen and Tetsuo Handa -- this change is splitting up the
> path_truncate LSM hook into a path_truncate and file_truncate variant,
> one operating on the path as before, and one operating on a struct
> file*. As a result, AppArmor and TOMOYO need to implement the
> file-based hook as well and treat it the same as before by looking at
> the file's ->f_path. Does this change seem reasonable to you?

Regarding TOMOYO part,

Acked-by: Tetsuo Handa <penguin-kernel@I-love.SAKURA.ne.jp>


^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook
  2022-09-08 19:58 ` [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook Günther Noack
  2022-09-08 20:09   ` Paul Moore
  2022-09-08 20:28   ` Günther Noack
@ 2022-09-09  3:37   ` John Johansen
  2022-09-09 13:50   ` Mickaël Salaün
  3 siblings, 0 replies; 39+ messages in thread
From: John Johansen @ 2022-09-09  3:37 UTC (permalink / raw)
  To: Günther Noack, linux-security-module
  Cc: Mickaël Salaün, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On 9/8/22 12:58, Günther Noack wrote:
> Like path_truncate, the file_truncate hook also restricts file
> truncation, but is called in the cases where truncation is attempted
> on an already-opened file.
> 
> This is required in a subsequent commit to handle ftruncate()
> operations differently to truncate() operations.
> 
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>

Acked-by: John Johansen <john.johansen@canonical.com>

> ---
>   fs/namei.c                    |  6 +++---
>   fs/open.c                     |  4 ++--
>   include/linux/lsm_hook_defs.h |  1 +
>   include/linux/security.h      |  6 ++++++
>   security/apparmor/lsm.c       |  6 ++++++
>   security/security.c           |  5 +++++
>   security/tomoyo/tomoyo.c      | 13 +++++++++++++
>   7 files changed, 36 insertions(+), 5 deletions(-)
> 
> diff --git a/fs/namei.c b/fs/namei.c
> index 53b4bc094db2..52105873d1f8 100644
> --- a/fs/namei.c
> +++ b/fs/namei.c
> @@ -53,8 +53,8 @@
>    * The new code replaces the old recursive symlink resolution with
>    * an iterative one (in case of non-nested symlink chains).  It does
>    * this with calls to <fs>_follow_link().
> - * As a side effect, dir_namei(), _namei() and follow_link() are now
> - * replaced with a single function lookup_dentry() that can handle all
> + * As a side effect, dir_namei(), _namei() and follow_link() are now
> + * replaced with a single function lookup_dentry() that can handle all
>    * the special cases of the former code.
>    *
>    * With the new dcache, the pathname is stored at each inode, at least as
> @@ -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 8a813fa5ca56..0831433e493a 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);
> @@ -1271,7 +1271,7 @@ struct file *filp_open(const char *filename, int flags, umode_t mode)
>   {
>   	struct filename *name = getname_kernel(filename);
>   	struct file *file = ERR_CAST(name);
> -	
> +
>   	if (!IS_ERR(name)) {
>   		file = file_open_name(name, flags, mode);
>   		putname(name);
> diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
> index 60fff133c0b1..dee35ab253ba 100644
> --- a/include/linux/lsm_hook_defs.h
> +++ b/include/linux/lsm_hook_defs.h
> @@ -177,6 +177,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/security.h b/include/linux/security.h
> index 7bd0c490703d..f80b23382dd9 100644
> --- a/include/linux/security.h
> +++ b/include/linux/security.h
> @@ -394,6 +394,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);
> @@ -1011,6 +1012,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/security/apparmor/lsm.c b/security/apparmor/lsm.c
> index e29cade7b662..98ecb7f221b8 100644
> --- a/security/apparmor/lsm.c
> +++ b/security/apparmor/lsm.c
> @@ -329,6 +329,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)
>   {
> @@ -1232,6 +1237,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/security.c b/security/security.c
> index 4b95de24bc8d..e491120c48ba 100644
> --- a/security/security.c
> +++ b/security/security.c
> @@ -1210,6 +1210,11 @@ int security_path_truncate(const struct path *path)
>   	return call_int_hook(path_truncate, 0, path);
>   }
>   
> +int security_file_truncate(struct file *file)
> +{
> +	return call_int_hook(file_truncate, 0, file);
> +}
> +
>   int security_path_chmod(const struct path *path, umode_t mode)
>   {
>   	if (unlikely(IS_PRIVATE(d_backing_inode(path->dentry))))
> 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
> @@ -134,6 +134,18 @@ static int tomoyo_path_truncate(const struct path *path)
>   	return tomoyo_path_perm(TOMOYO_TYPE_TRUNCATE, path, NULL);
>   }
>   
> +/**
> + * 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().
>    *
> @@ -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),


^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook
  2022-09-08 19:58 ` [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook Günther Noack
                     ` (2 preceding siblings ...)
  2022-09-09  3:37   ` John Johansen
@ 2022-09-09 13:50   ` Mickaël Salaün
  3 siblings, 0 replies; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-09 13:50 UTC (permalink / raw)
  To: Günther Noack, linux-security-module
  Cc: James Morris, Paul Moore, Serge E . Hallyn, linux-fsdevel,
	Konstantin Meskhidze

Great! Just the unintended hunks to remove as pointed by Paul.


On 08/09/2022 21:58, Günther Noack wrote:
> Like path_truncate, the file_truncate hook also restricts file
> truncation, but is called in the cases where truncation is attempted
> on an already-opened file.
> 
> This is required in a subsequent commit to handle ftruncate()
> operations differently to truncate() operations.
> 
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
>   fs/namei.c                    |  6 +++---
>   fs/open.c                     |  4 ++--
>   include/linux/lsm_hook_defs.h |  1 +
>   include/linux/security.h      |  6 ++++++
>   security/apparmor/lsm.c       |  6 ++++++
>   security/security.c           |  5 +++++
>   security/tomoyo/tomoyo.c      | 13 +++++++++++++
>   7 files changed, 36 insertions(+), 5 deletions(-)
> 
> diff --git a/fs/namei.c b/fs/namei.c
> index 53b4bc094db2..52105873d1f8 100644
> --- a/fs/namei.c
> +++ b/fs/namei.c
> @@ -53,8 +53,8 @@
>    * The new code replaces the old recursive symlink resolution with
>    * an iterative one (in case of non-nested symlink chains).  It does
>    * this with calls to <fs>_follow_link().
> - * As a side effect, dir_namei(), _namei() and follow_link() are now
> - * replaced with a single function lookup_dentry() that can handle all
> + * As a side effect, dir_namei(), _namei() and follow_link() are now
> + * replaced with a single function lookup_dentry() that can handle all
>    * the special cases of the former code.
>    *
>    * With the new dcache, the pathname is stored at each inode, at least as
> @@ -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 8a813fa5ca56..0831433e493a 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);
> @@ -1271,7 +1271,7 @@ struct file *filp_open(const char *filename, int flags, umode_t mode)
>   {
>   	struct filename *name = getname_kernel(filename);
>   	struct file *file = ERR_CAST(name);
> -	
> +
>   	if (!IS_ERR(name)) {
>   		file = file_open_name(name, flags, mode);
>   		putname(name);
> diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
> index 60fff133c0b1..dee35ab253ba 100644
> --- a/include/linux/lsm_hook_defs.h
> +++ b/include/linux/lsm_hook_defs.h
> @@ -177,6 +177,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/security.h b/include/linux/security.h
> index 7bd0c490703d..f80b23382dd9 100644
> --- a/include/linux/security.h
> +++ b/include/linux/security.h
> @@ -394,6 +394,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);
> @@ -1011,6 +1012,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/security/apparmor/lsm.c b/security/apparmor/lsm.c
> index e29cade7b662..98ecb7f221b8 100644
> --- a/security/apparmor/lsm.c
> +++ b/security/apparmor/lsm.c
> @@ -329,6 +329,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)
>   {
> @@ -1232,6 +1237,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/security.c b/security/security.c
> index 4b95de24bc8d..e491120c48ba 100644
> --- a/security/security.c
> +++ b/security/security.c
> @@ -1210,6 +1210,11 @@ int security_path_truncate(const struct path *path)
>   	return call_int_hook(path_truncate, 0, path);
>   }
>   
> +int security_file_truncate(struct file *file)
> +{
> +	return call_int_hook(file_truncate, 0, file);
> +}
> +
>   int security_path_chmod(const struct path *path, umode_t mode)
>   {
>   	if (unlikely(IS_PRIVATE(d_backing_inode(path->dentry))))
> 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
> @@ -134,6 +134,18 @@ static int tomoyo_path_truncate(const struct path *path)
>   	return tomoyo_path_perm(TOMOYO_TYPE_TRUNCATE, path, NULL);
>   }
>   
> +/**
> + * 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().
>    *
> @@ -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),

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/5] landlock: Support file truncation
  2022-09-08 19:58 ` [PATCH v6 2/5] landlock: Support file truncation Günther Noack
@ 2022-09-09 13:51   ` Mickaël Salaün
  2022-09-12 15:28     ` Günther Noack
  2022-09-12 19:41   ` Mickaël Salaün
  1 sibling, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-09 13:51 UTC (permalink / raw)
  To: Günther Noack, linux-security-module
  Cc: James Morris, Paul Moore, Serge E . Hallyn, linux-fsdevel,
	Konstantin Meskhidze


On 08/09/2022 21:58, Günther Noack wrote:
> Introduce the LANDLOCK_ACCESS_FS_TRUNCATE flag for file truncation.
> 
> This flag hooks into the path_truncate LSM hook and covers file
> truncation using truncate(2), ftruncate(2), open(2) with O_TRUNC, as
> well as creat().
> 
> This change also increments the Landlock ABI version, updates
> corresponding selftests, and updates code documentation to document
> the flag.
> 
> The following operations are restricted:
> 
> open(): requires the LANDLOCK_ACCESS_FS_TRUNCATE right if a file gets
> implicitly truncated as part of the open() (e.g. using O_TRUNC).
> 
> Notable special cases:
> * open(..., O_RDONLY|O_TRUNC) can truncate files as well in Linux
> * open() with O_TRUNC does *not* need the TRUNCATE right when it
>    creates a new file.
> 
> truncate() (on a path): requires the LANDLOCK_ACCESS_FS_TRUNCATE
> right.
> 
> ftruncate() (on a file): requires that the file had the TRUNCATE right
> when it was previously opened.
> 
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
>   include/uapi/linux/landlock.h                | 18 ++--
>   security/landlock/fs.c                       | 88 +++++++++++++++++++-
>   security/landlock/fs.h                       | 18 ++++
>   security/landlock/limits.h                   |  2 +-
>   security/landlock/setup.c                    |  1 +
>   security/landlock/syscalls.c                 |  2 +-
>   tools/testing/selftests/landlock/base_test.c |  2 +-
>   tools/testing/selftests/landlock/fs_test.c   |  7 +-
>   8 files changed, 124 insertions(+), 14 deletions(-)
> 
> diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
> index 23df4e0e8ace..8c0124c5cbe6 100644
> --- a/include/uapi/linux/landlock.h
> +++ b/include/uapi/linux/landlock.h
> @@ -95,8 +95,16 @@ 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`. The right to truncate a file gets carried along with an opened
> + *   file descriptor for the purpose of :manpage:`ftruncate(2)`.

You can add a bit to explain that it is the same behavior as for 
LANDLOCK_ACCESS_FS_{READ,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 +147,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 +167,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/security/landlock/fs.c b/security/landlock/fs.c
> index a9dbd99d9ee7..1b546edf69a6 100644
> --- a/security/landlock/fs.c
> +++ b/security/landlock/fs.c
> @@ -146,7 +146,8 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
>   #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 */
>   
>   /*
> @@ -761,6 +762,47 @@ static bool collect_domain_accesses(
>   	return ret;
>   }
>   
> +/**
> + * get_path_access_rights - Returns the subset of rights in access_request
> + * which are permitted for the given path.
> + *
> + * @domain: The domain that defines the current restrictions.
> + * @path: The path to get access rights for.
> + * @access_request: The rights we are interested in.
> + *
> + * Returns: The access mask of the rights that are permitted on the given path,
> + * which are also a subset of access_request (to save some calculation time).
> + */
> +static inline access_mask_t
> +get_path_access_rights(const struct landlock_ruleset *const domain,
> +		       const struct path *const path,
> +		       access_mask_t access_request)
> +{
> +	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
> +	unsigned long access_bit;
> +	unsigned long access_req;

unsigned long access_bit, long access_req;


> +
> +	init_layer_masks(domain, access_request, &layer_masks);
> +	if (!check_access_path_dual(domain, path, access_request, &layer_masks,
> +				    NULL, 0, NULL, NULL)) {
> +		/*
> +		 * Return immediately for successful accesses and for cases

Returns


> +		 * where everything is permitted because the path belongs to an
> +		 * internal filesystem.
> +		 */
> +		return access_request;
> +	}
> +
> +	access_req = access_request;
> +	for_each_set_bit(access_bit, &access_req, ARRAY_SIZE(layer_masks)) {
> +		if (layer_masks[access_bit]) {
> +			/* If any layer vetoed the access right, remove it. */
> +			access_request &= ~BIT_ULL(access_bit);
> +		}
> +	}
> +	return access_request;
> +}
> +
>   /**
>    * current_check_refer_path - Check if a rename or link action is allowed
>    *
> @@ -1142,6 +1184,11 @@ 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)
> @@ -1159,22 +1206,55 @@ static inline access_mask_t get_file_access(const struct file *const file)
>   	/* __FMODE_EXEC is indeed part of f_flags, not f_mode. */
>   	if (file->f_flags & __FMODE_EXEC)
>   		access |= LANDLOCK_ACCESS_FS_EXECUTE;
> +

Not needed.


>   	return access;
>   }
>   
>   static int hook_file_open(struct file *const file)
>   {
> +	access_mask_t access_req, access_rights;
> +	const access_mask_t optional_rights = LANDLOCK_ACCESS_FS_TRUNCATE;
>   	const struct landlock_ruleset *const dom =
>   		landlock_get_current_domain();
>   
> -	if (!dom)
> +	if (!dom) {
> +		/* Grant all rights. */

Something like:
Grants all rights, even if most of them are not checked here, it is more 
consistent.


> +		landlock_file(file)->rights = LANDLOCK_MASK_ACCESS_FS;
>   		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
>   	 * evolution.
>   	 */
> -	return check_access_path(dom, &file->f_path, get_file_access(file));
> +	access_req = get_file_access(file);
> +	access_rights = get_path_access_rights(dom, &file->f_path,
> +					       access_req | optional_rights);
> +	if (access_req & ~access_rights)
> +		return -EACCES;
> +
> +	/*
> +	 * 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)->rights = access_rights;
> +

Style preferences, but why do you use a new line here? I try to group 
code blocks until the return.


> +	return 0;
> +}
> +
> +static int hook_file_truncate(struct file *const file)
> +{
> +	/*
> +	 * We permit truncation if the truncation right was available at the

Allows truncation if the related right was…


> +	 * time of opening the file.

…to get a consistent access check as for read, write and execute operations.

This kind of explanation could be used to complete the documentation as 
well. The idea being to mimic the file mode check.


> +	 */
> +	if (!(landlock_file(file)->rights & LANDLOCK_ACCESS_FS_TRUNCATE))

I prefer to invert the "if" logic and return -EACCES by default.


> +		return -EACCES;
> +
> +	return 0;
>   }
>   
>   static struct security_hook_list landlock_hooks[] __lsm_ro_after_init = {
> @@ -1194,6 +1274,8 @@ 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_truncate, hook_file_truncate),
>   
>   	LSM_HOOK_INIT(file_open, hook_file_open),
>   };
> diff --git a/security/landlock/fs.h b/security/landlock/fs.h
> index 8db7acf9109b..275ba5375839 100644
> --- a/security/landlock/fs.h
> +++ b/security/landlock/fs.h
> @@ -36,6 +36,18 @@ struct landlock_inode_security {
>   	struct landlock_object __rcu *object;
>   };
>   
> +/**
> + * 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 {
> +	access_mask_t rights;

I think it would make it more consistent to name it "access" to be in 
line with struct landlock_layer and other types.


> +};
> +
>   /**
>    * struct landlock_superblock_security - Superblock security blob
>    *
> @@ -50,6 +62,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 735a0865ea11..f4d6fc7ed17f 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/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
> index da9290817866..72cdae277b02 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,
>   					     LANDLOCK_CREATE_RULESET_VERSION));
>   
>   	ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
> diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
> index 45de42a027c5..87b28d14a1aa 100644
> --- a/tools/testing/selftests/landlock/fs_test.c
> +++ b/tools/testing/selftests/landlock/fs_test.c
> @@ -406,9 +406,10 @@ TEST_F_FORK(layout1, inval)
>   #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)
>   
> -#define ACCESS_LAST LANDLOCK_ACCESS_FS_REFER
> +#define ACCESS_LAST LANDLOCK_ACCESS_FS_TRUNCATE
>   
>   #define ACCESS_ALL ( \
>   	ACCESS_FILE | \
> @@ -422,7 +423,7 @@ TEST_F_FORK(layout1, inval)
>   	LANDLOCK_ACCESS_FS_MAKE_FIFO | \
>   	LANDLOCK_ACCESS_FS_MAKE_BLOCK | \
>   	LANDLOCK_ACCESS_FS_MAKE_SYM | \
> -	ACCESS_LAST)
> +	LANDLOCK_ACCESS_FS_REFER)
>   
>   /* clang-format on */
>   

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 5/5] landlock: Document Landlock's file truncation support
  2022-09-08 19:58 ` [PATCH v6 5/5] landlock: Document Landlock's file truncation support Günther Noack
@ 2022-09-09 13:51   ` Mickaël Salaün
  2022-09-12 15:46     ` Günther Noack
  2022-09-12 19:15   ` Mickaël Salaün
  1 sibling, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-09 13:51 UTC (permalink / raw)
  To: Günther Noack, linux-security-module
  Cc: James Morris, Paul Moore, Serge E . Hallyn, linux-fsdevel,
	Konstantin Meskhidze


On 08/09/2022 21:58, Günther Noack wrote:
> Use the LANDLOCK_ACCESS_FS_TRUNCATE flag in the tutorial.
> 
> Adapt the backwards compatibility example and discussion to remove the
> truncation flag where needed.
> 
> Point out potential surprising behaviour related to truncate.
> 
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
>   Documentation/userspace-api/landlock.rst | 62 +++++++++++++++++++++---
>   1 file changed, 54 insertions(+), 8 deletions(-)
> 
> diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
> index b8ea59493964..57802fd1e09b 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: May 2022
> +:Date: September 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,26 @@ 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) {
> -        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
> +    switch (abi) {
> +    case -1:
> +            perror("The running kernel does not enable to use Landlock");
> +            return 1;
> +    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 +138,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.

This fix is correct, but it should not be part of this series. FYI, I 
have a patch almost ready to fix some documentation style issues. Please 
remove this hunk for the next series. I'll deal with the merge conflicts 
if any.


>   
>   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
> @@ -251,6 +262,32 @@ 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

I investigated and in fact we should use double backquotes almost 
everywhere because it render the same as when using "%" in header files. 
Please change this for the next series. I'll do the same on the patch I 
just talk about.


> +`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)`.  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 don't have an enforced Landlock ruleset.

Good addition. Please do not use contractions ("don't").


> +
>   Compatibility
>   =============
>   
> @@ -397,6 +434,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

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/5] landlock: Support file truncation
  2022-09-09 13:51   ` Mickaël Salaün
@ 2022-09-12 15:28     ` Günther Noack
  2022-09-12 18:37       ` Mickaël Salaün
  0 siblings, 1 reply; 39+ messages in thread
From: Günther Noack @ 2022-09-12 15:28 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Fri, Sep 09, 2022 at 03:51:16PM +0200, Mickaël Salaün wrote:
> 
> On 08/09/2022 21:58, Günther Noack wrote:
> > Introduce the LANDLOCK_ACCESS_FS_TRUNCATE flag for file truncation.
> > 
> > This flag hooks into the path_truncate LSM hook and covers file
> > truncation using truncate(2), ftruncate(2), open(2) with O_TRUNC, as
> > well as creat().
> > 
> > This change also increments the Landlock ABI version, updates
> > corresponding selftests, and updates code documentation to document
> > the flag.
> > 
> > The following operations are restricted:
> > 
> > open(): requires the LANDLOCK_ACCESS_FS_TRUNCATE right if a file gets
> > implicitly truncated as part of the open() (e.g. using O_TRUNC).
> > 
> > Notable special cases:
> > * open(..., O_RDONLY|O_TRUNC) can truncate files as well in Linux
> > * open() with O_TRUNC does *not* need the TRUNCATE right when it
> >    creates a new file.
> > 
> > truncate() (on a path): requires the LANDLOCK_ACCESS_FS_TRUNCATE
> > right.
> > 
> > ftruncate() (on a file): requires that the file had the TRUNCATE right
> > when it was previously opened.
> > 
> > Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> > ---
> >   include/uapi/linux/landlock.h                | 18 ++--
> >   security/landlock/fs.c                       | 88 +++++++++++++++++++-
> >   security/landlock/fs.h                       | 18 ++++
> >   security/landlock/limits.h                   |  2 +-
> >   security/landlock/setup.c                    |  1 +
> >   security/landlock/syscalls.c                 |  2 +-
> >   tools/testing/selftests/landlock/base_test.c |  2 +-
> >   tools/testing/selftests/landlock/fs_test.c   |  7 +-
> >   8 files changed, 124 insertions(+), 14 deletions(-)
> > 
> > diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
> > index 23df4e0e8ace..8c0124c5cbe6 100644
> > --- a/include/uapi/linux/landlock.h
> > +++ b/include/uapi/linux/landlock.h
> > + * - %LANDLOCK_ACCESS_FS_TRUNCATE: Truncate a file with :manpage:`truncate(2)`,
> > + *   :manpage:`ftruncate(2)`, :manpage:`creat(2)`, or :manpage:`open(2)` with
> > + *   `O_TRUNC`. The right to truncate a file gets carried along with an opened
> > + *   file descriptor for the purpose of :manpage:`ftruncate(2)`.
> 
> You can add a bit to explain that it is the same behavior as for
> LANDLOCK_ACCESS_FS_{READ,WRITE}_FILE .

Done.

> > diff --git a/security/landlock/fs.c b/security/landlock/fs.c
> > index a9dbd99d9ee7..1b546edf69a6 100644
> > --- a/security/landlock/fs.c
> > +++ b/security/landlock/fs.c
> > +static inline access_mask_t
> > +get_path_access_rights(const struct landlock_ruleset *const domain,
> > +		       const struct path *const path,
> > +		       access_mask_t access_request)
> > +{
> > +	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
> > +	unsigned long access_bit;
> > +	unsigned long access_req;
> 
> unsigned long access_bit, long access_req;

Done. Made it unsigned long access_bit, access_req;

> > +	init_layer_masks(domain, access_request, &layer_masks);
> > +	if (!check_access_path_dual(domain, path, access_request, &layer_masks,
> > +				    NULL, 0, NULL, NULL)) {
> > +		/*
> > +		 * Return immediately for successful accesses and for cases
> 
> Returns

Done.

> > +		 * where everything is permitted because the path belongs to an
> > +		 * internal filesystem.
> > +		 */
> > +		return access_request;
> > +	}
> > +
> > +	access_req = access_request;
> > +	for_each_set_bit(access_bit, &access_req, ARRAY_SIZE(layer_masks)) {
> > +		if (layer_masks[access_bit]) {
> > +			/* If any layer vetoed the access right, remove it. */
> > +			access_request &= ~BIT_ULL(access_bit);
> > +		}
> > +	}
> > +	return access_request;
> > +}
> > +
> >   /**
> >    * current_check_refer_path - Check if a rename or link action is allowed
> >    *
> > @@ -1142,6 +1184,11 @@ 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)
> > @@ -1159,22 +1206,55 @@ static inline access_mask_t get_file_access(const struct file *const file)
> >   	/* __FMODE_EXEC is indeed part of f_flags, not f_mode. */
> >   	if (file->f_flags & __FMODE_EXEC)
> >   		access |= LANDLOCK_ACCESS_FS_EXECUTE;
> > +
> 
> Not needed.

Done.

> >   	return access;
> >   }
> >   static int hook_file_open(struct file *const file)
> >   {
> > +	access_mask_t access_req, access_rights;
> > +	const access_mask_t optional_rights = LANDLOCK_ACCESS_FS_TRUNCATE;
> >   	const struct landlock_ruleset *const dom =
> >   		landlock_get_current_domain();
> > -	if (!dom)
> > +	if (!dom) {
> > +		/* Grant all rights. */
> 
> Something like:
> Grants all rights, even if most of them are not checked here, it is more
> consistent.

Done.

> > +		landlock_file(file)->rights = LANDLOCK_MASK_ACCESS_FS;
> >   		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
> >   	 * evolution.
> >   	 */
> > -	return check_access_path(dom, &file->f_path, get_file_access(file));
> > +	access_req = get_file_access(file);
> > +	access_rights = get_path_access_rights(dom, &file->f_path,
> > +					       access_req | optional_rights);
> > +	if (access_req & ~access_rights)
> > +		return -EACCES;
> > +
> > +	/*
> > +	 * 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)->rights = access_rights;
> > +
> 
> Style preferences, but why do you use a new line here? I try to group code
> blocks until the return.

Thanks, done. I just do this habitually and overlooked that I was at
odds with the surrounding style. I don't have a strong preference.

> > +	return 0;
> > +}
> > +
> > +static int hook_file_truncate(struct file *const file)
> > +{
> > +	/*
> > +	 * We permit truncation if the truncation right was available at the
> 
> Allows truncation if the related right was…
> 
> 
> > +	 * time of opening the file.
> 
> …to get a consistent access check as for read, write and execute operations.

Done.

I'm also adding this note here:

  Note: For checks done based on the file's Landlock rights, 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.

to explain that this is why we don't check for "if (!dom)" as we do in
other cases.


> This kind of explanation could be used to complete the documentation as
> well. The idea being to mimic the file mode check.

Added it to the documentation.

> 
> 
> > +	 */
> > +	if (!(landlock_file(file)->rights & LANDLOCK_ACCESS_FS_TRUNCATE))
> 
> I prefer to invert the "if" logic and return -EACCES by default.

Done. Thanks for pointing it out.

> > +		return -EACCES;
> > +
> > +	return 0;
> >   }


> > diff --git a/security/landlock/fs.h b/security/landlock/fs.h
> > index 8db7acf9109b..275ba5375839 100644
> > --- a/security/landlock/fs.h
> > +++ b/security/landlock/fs.h
> > @@ -36,6 +36,18 @@ struct landlock_inode_security {
> >   	struct landlock_object __rcu *object;
> >   };
> > +/**
> > + * 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 {
> > +	access_mask_t rights;
> 
> I think it would make it more consistent to name it "access" to be in line
> with struct landlock_layer and other types.

Done.

I also added a brief documentation for the access field, to point out
that this is not the *full* set of rights which was available at
open() time, but it's just the subset of rights that is needed to
authorize later operations on the file:

  @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.

Thanks for the review! Fixes will be in the next version.

-Günther

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 5/5] landlock: Document Landlock's file truncation support
  2022-09-09 13:51   ` Mickaël Salaün
@ 2022-09-12 15:46     ` Günther Noack
  2022-09-12 17:47       ` Mickaël Salaün
  0 siblings, 1 reply; 39+ messages in thread
From: Günther Noack @ 2022-09-12 15:46 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Fri, Sep 09, 2022 at 03:51:35PM +0200, Mickaël Salaün wrote:
> 
> On 08/09/2022 21:58, Günther Noack wrote:
> > Use the LANDLOCK_ACCESS_FS_TRUNCATE flag in the tutorial.
> > 
> > Adapt the backwards compatibility example and discussion to remove the
> > truncation flag where needed.
> > 
> > Point out potential surprising behaviour related to truncate.
> > 
> > Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> > ---
> >   Documentation/userspace-api/landlock.rst | 62 +++++++++++++++++++++---
> >   1 file changed, 54 insertions(+), 8 deletions(-)
> > 
> > diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
> > index b8ea59493964..57802fd1e09b 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: May 2022
> > +:Date: September 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,26 @@ 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) {
> > -        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
> > +    switch (abi) {
> > +    case -1:
> > +            perror("The running kernel does not enable to use Landlock");
> > +            return 1;
> > +    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 +138,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.
> 
> This fix is correct, but it should not be part of this series. FYI, I have a
> patch almost ready to fix some documentation style issues. Please remove
> this hunk for the next series. I'll deal with the merge conflicts if any.

Can you please clarify what part of it should not be part of this
series?

In this hunk, I've started using double backquote, but I've also
changed the meaning of the sentence slightly so that it is still
correct when the truncate right is introduced.

It is still correct that the backwards compatibility check is not
required because LANDLOCK_ACCESS_FS_REFER is not allowed by any rule.
But with the new truncate flag, LANDLOCK_ACCESS_FS_TRUNCATE may also
not be allowed by any rule so that we can skip this check.

Should I remove this hunk entirely?

Or maybe rather phrase it like

  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` and
  `LANDLOCK_ACCESS_FS_TRUNCATE` are not allowed by any rule.
 
?

> >   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
> > @@ -251,6 +262,32 @@ 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
> 
> I investigated and in fact we should use double backquotes almost everywhere
> because it render the same as when using "%" in header files. Please change
> this for the next series. I'll do the same on the patch I just talk about.

Done. I'm changing double backquote style only in the hunks I touched
so that it'll be easier to merge.

> 
> 
> > +`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)`.  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 don't have an enforced Landlock ruleset.
> 
> Good addition. Please do not use contractions ("don't").

Done, thanks.

> 
> 
> > +
> >   Compatibility
> >   =============
> > @@ -397,6 +434,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.

Changed backquote style here as well.

Thanks for the review!

—Günther

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 5/5] landlock: Document Landlock's file truncation support
  2022-09-12 15:46     ` Günther Noack
@ 2022-09-12 17:47       ` Mickaël Salaün
  2022-09-12 19:05         ` Günther Noack
  0 siblings, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-12 17:47 UTC (permalink / raw)
  To: Günther Noack
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze


On 12/09/2022 17:46, Günther Noack wrote:
> On Fri, Sep 09, 2022 at 03:51:35PM +0200, Mickaël Salaün wrote:
>>
>> On 08/09/2022 21:58, Günther Noack wrote:
>>> Use the LANDLOCK_ACCESS_FS_TRUNCATE flag in the tutorial.
>>>
>>> Adapt the backwards compatibility example and discussion to remove the
>>> truncation flag where needed.
>>>
>>> Point out potential surprising behaviour related to truncate.
>>>
>>> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
>>> ---
>>>    Documentation/userspace-api/landlock.rst | 62 +++++++++++++++++++++---
>>>    1 file changed, 54 insertions(+), 8 deletions(-)
>>>
>>> diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
>>> index b8ea59493964..57802fd1e09b 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: May 2022
>>> +:Date: September 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,26 @@ 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) {
>>> -        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
>>> +    switch (abi) {
>>> +    case -1:
>>> +            perror("The running kernel does not enable to use Landlock");
>>> +            return 1;
>>> +    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 +138,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.
>>
>> This fix is correct, but it should not be part of this series. FYI, I have a
>> patch almost ready to fix some documentation style issues. Please remove
>> this hunk for the next series. I'll deal with the merge conflicts if any.
> 
> Can you please clarify what part of it should not be part of this
> series?

My mistake, I guess I was reviewing something else… I was thinking about 
style changes, but it is not the case here. Using "``" is correct.


> 
> In this hunk, I've started using double backquote, but I've also
> changed the meaning of the sentence slightly so that it is still
> correct when the truncate right is introduced.
> 
> It is still correct that the backwards compatibility check is not
> required because LANDLOCK_ACCESS_FS_REFER is not allowed by any rule.
> But with the new truncate flag, LANDLOCK_ACCESS_FS_TRUNCATE may also
> not be allowed by any rule so that we can skip this check.
> 
> Should I remove this hunk entirely?

Keep your changes, it's better like this.


> 
> Or maybe rather phrase it like
> 
>    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` and
>    `LANDLOCK_ACCESS_FS_TRUNCATE` are not allowed by any rule.
>   
> ?

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/5] landlock: Support file truncation
  2022-09-12 15:28     ` Günther Noack
@ 2022-09-12 18:37       ` Mickaël Salaün
  2022-09-12 19:04         ` Günther Noack
  0 siblings, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-12 18:37 UTC (permalink / raw)
  To: Günther Noack
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze



On 12/09/2022 17:28, Günther Noack wrote:
> On Fri, Sep 09, 2022 at 03:51:16PM +0200, Mickaël Salaün wrote:
>>
>> On 08/09/2022 21:58, Günther Noack wrote:
>>> Introduce the LANDLOCK_ACCESS_FS_TRUNCATE flag for file truncation.
>>>
>>> This flag hooks into the path_truncate LSM hook and covers file
>>> truncation using truncate(2), ftruncate(2), open(2) with O_TRUNC, as
>>> well as creat().
>>>
>>> This change also increments the Landlock ABI version, updates
>>> corresponding selftests, and updates code documentation to document
>>> the flag.
>>>
>>> The following operations are restricted:
>>>
>>> open(): requires the LANDLOCK_ACCESS_FS_TRUNCATE right if a file gets
>>> implicitly truncated as part of the open() (e.g. using O_TRUNC).
>>>
>>> Notable special cases:
>>> * open(..., O_RDONLY|O_TRUNC) can truncate files as well in Linux
>>> * open() with O_TRUNC does *not* need the TRUNCATE right when it
>>>     creates a new file.
>>>
>>> truncate() (on a path): requires the LANDLOCK_ACCESS_FS_TRUNCATE
>>> right.
>>>
>>> ftruncate() (on a file): requires that the file had the TRUNCATE right
>>> when it was previously opened.
>>>
>>> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
>>> ---
>>>    include/uapi/linux/landlock.h                | 18 ++--
>>>    security/landlock/fs.c                       | 88 +++++++++++++++++++-
>>>    security/landlock/fs.h                       | 18 ++++
>>>    security/landlock/limits.h                   |  2 +-
>>>    security/landlock/setup.c                    |  1 +
>>>    security/landlock/syscalls.c                 |  2 +-
>>>    tools/testing/selftests/landlock/base_test.c |  2 +-
>>>    tools/testing/selftests/landlock/fs_test.c   |  7 +-
>>>    8 files changed, 124 insertions(+), 14 deletions(-)
>>>
>>> diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
>>> index 23df4e0e8ace..8c0124c5cbe6 100644
>>> --- a/include/uapi/linux/landlock.h
>>> +++ b/include/uapi/linux/landlock.h
>>> + * - %LANDLOCK_ACCESS_FS_TRUNCATE: Truncate a file with :manpage:`truncate(2)`,
>>> + *   :manpage:`ftruncate(2)`, :manpage:`creat(2)`, or :manpage:`open(2)` with
>>> + *   `O_TRUNC`. The right to truncate a file gets carried along with an opened
>>> + *   file descriptor for the purpose of :manpage:`ftruncate(2)`.
>>
>> You can add a bit to explain that it is the same behavior as for
>> LANDLOCK_ACCESS_FS_{READ,WRITE}_FILE .
> 
> Done.
> 
>>> diff --git a/security/landlock/fs.c b/security/landlock/fs.c
>>> index a9dbd99d9ee7..1b546edf69a6 100644
>>> --- a/security/landlock/fs.c
>>> +++ b/security/landlock/fs.c
>>> +static inline access_mask_t
>>> +get_path_access_rights(const struct landlock_ruleset *const domain,
>>> +		       const struct path *const path,
>>> +		       access_mask_t access_request)
>>> +{
>>> +	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
>>> +	unsigned long access_bit;
>>> +	unsigned long access_req;
>>
>> unsigned long access_bit, long access_req;
> 
> Done. Made it unsigned long access_bit, access_req;
> 
>>> +	init_layer_masks(domain, access_request, &layer_masks);
>>> +	if (!check_access_path_dual(domain, path, access_request, &layer_masks,
>>> +				    NULL, 0, NULL, NULL)) {
>>> +		/*
>>> +		 * Return immediately for successful accesses and for cases
>>
>> Returns
> 
> Done.
> 
>>> +		 * where everything is permitted because the path belongs to an
>>> +		 * internal filesystem.
>>> +		 */
>>> +		return access_request;
>>> +	}
>>> +
>>> +	access_req = access_request;
>>> +	for_each_set_bit(access_bit, &access_req, ARRAY_SIZE(layer_masks)) {
>>> +		if (layer_masks[access_bit]) {
>>> +			/* If any layer vetoed the access right, remove it. */
>>> +			access_request &= ~BIT_ULL(access_bit);
>>> +		}
>>> +	}
>>> +	return access_request;
>>> +}
>>> +
>>>    /**
>>>     * current_check_refer_path - Check if a rename or link action is allowed
>>>     *
>>> @@ -1142,6 +1184,11 @@ 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)
>>> @@ -1159,22 +1206,55 @@ static inline access_mask_t get_file_access(const struct file *const file)
>>>    	/* __FMODE_EXEC is indeed part of f_flags, not f_mode. */
>>>    	if (file->f_flags & __FMODE_EXEC)
>>>    		access |= LANDLOCK_ACCESS_FS_EXECUTE;
>>> +
>>
>> Not needed.
> 
> Done.
> 
>>>    	return access;
>>>    }
>>>    static int hook_file_open(struct file *const file)
>>>    {
>>> +	access_mask_t access_req, access_rights;
>>> +	const access_mask_t optional_rights = LANDLOCK_ACCESS_FS_TRUNCATE;
>>>    	const struct landlock_ruleset *const dom =
>>>    		landlock_get_current_domain();
>>> -	if (!dom)
>>> +	if (!dom) {
>>> +		/* Grant all rights. */
>>
>> Something like:
>> Grants all rights, even if most of them are not checked here, it is more
>> consistent.
> 
> Done.
> 
>>> +		landlock_file(file)->rights = LANDLOCK_MASK_ACCESS_FS;
>>>    		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
>>>    	 * evolution.
>>>    	 */
>>> -	return check_access_path(dom, &file->f_path, get_file_access(file));
>>> +	access_req = get_file_access(file);
>>> +	access_rights = get_path_access_rights(dom, &file->f_path,
>>> +					       access_req | optional_rights);
>>> +	if (access_req & ~access_rights)
>>> +		return -EACCES;
>>> +
>>> +	/*
>>> +	 * 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)->rights = access_rights;
>>> +
>>
>> Style preferences, but why do you use a new line here? I try to group code
>> blocks until the return.
> 
> Thanks, done. I just do this habitually and overlooked that I was at
> odds with the surrounding style. I don't have a strong preference.
> 
>>> +	return 0;
>>> +}
>>> +
>>> +static int hook_file_truncate(struct file *const file)
>>> +{
>>> +	/*
>>> +	 * We permit truncation if the truncation right was available at the
>>
>> Allows truncation if the related right was…
>>
>>
>>> +	 * time of opening the file.
>>
>> …to get a consistent access check as for read, write and execute operations.
> 
> Done.
> 
> I'm also adding this note here:
> 
>    Note: For checks done based on the file's Landlock rights, 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.
> 
> to explain that this is why we don't check for "if (!dom)" as we do in
> other cases.
> 
> 
>> This kind of explanation could be used to complete the documentation as
>> well. The idea being to mimic the file mode check.
> 
> Added it to the documentation.
> 
>>
>>
>>> +	 */
>>> +	if (!(landlock_file(file)->rights & LANDLOCK_ACCESS_FS_TRUNCATE))
>>
>> I prefer to invert the "if" logic and return -EACCES by default.
> 
> Done. Thanks for pointing it out.
> 
>>> +		return -EACCES;
>>> +
>>> +	return 0;
>>>    }
> 
> 
>>> diff --git a/security/landlock/fs.h b/security/landlock/fs.h
>>> index 8db7acf9109b..275ba5375839 100644
>>> --- a/security/landlock/fs.h
>>> +++ b/security/landlock/fs.h
>>> @@ -36,6 +36,18 @@ struct landlock_inode_security {
>>>    	struct landlock_object __rcu *object;
>>>    };
>>> +/**
>>> + * 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 {
>>> +	access_mask_t rights;
>>
>> I think it would make it more consistent to name it "access" to be in line
>> with struct landlock_layer and other types.
> 
> Done.

Hmm, actually, "allowed_access" is more explicit. We could use other 
access-related fields for other purposes (e.g. cache).


> 
> I also added a brief documentation for the access field, to point out
> that this is not the *full* set of rights which was available at
> open() time, but it's just the subset of rights that is needed to
> authorize later operations on the file:
> 
>    @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.

Good!

> 
> Thanks for the review! Fixes will be in the next version.
> 
> -Günther
> 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/5] landlock: Support file truncation
  2022-09-12 18:37       ` Mickaël Salaün
@ 2022-09-12 19:04         ` Günther Noack
  0 siblings, 0 replies; 39+ messages in thread
From: Günther Noack @ 2022-09-12 19:04 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Mon, Sep 12, 2022 at 08:37:09PM +0200, Mickaël Salaün wrote:
> 
> 
> On 12/09/2022 17:28, Günther Noack wrote:
> > On Fri, Sep 09, 2022 at 03:51:16PM +0200, Mickaël Salaün wrote:
> > > 
> > > On 08/09/2022 21:58, Günther Noack wrote:
> > > > diff --git a/security/landlock/fs.h b/security/landlock/fs.h
> > > > index 8db7acf9109b..275ba5375839 100644
> > > > --- a/security/landlock/fs.h
> > > > +++ b/security/landlock/fs.h
> > > > +/**
> > > > + * 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 {
> > > > +	access_mask_t rights;
> > > 
> > > I think it would make it more consistent to name it "access" to be in line
> > > with struct landlock_layer and other types.
> > 
> > Done.
> 
> Hmm, actually, "allowed_access" is more explicit. We could use other
> access-related fields for other purposes (e.g. cache).

Makes sense, renamed to allowed_access.

—Günther

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 4/5] samples/landlock: Extend sample tool to support LANDLOCK_ACCESS_FS_TRUNCATE
  2022-09-08 19:58 ` [PATCH v6 4/5] samples/landlock: Extend sample tool to support LANDLOCK_ACCESS_FS_TRUNCATE Günther Noack
@ 2022-09-12 19:05   ` Mickaël Salaün
  2022-09-12 19:07     ` Günther Noack
  0 siblings, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-12 19:05 UTC (permalink / raw)
  To: Günther Noack, linux-security-module
  Cc: James Morris, Paul Moore, Serge E . Hallyn, linux-fsdevel,
	Konstantin Meskhidze



On 08/09/2022 21:58, Günther Noack wrote:
> Update the sandboxer sample to restrict truncate actions. This is
> automatically enabled by default if the running kernel supports
> LANDLOCK_ACCESS_FS_TRUNCATE, expect for the paths listed in the

except for

> LL_FS_RW environment variable.
> 
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
>   samples/landlock/sandboxer.c | 23 ++++++++++++++---------
>   1 file changed, 14 insertions(+), 9 deletions(-)
> 
> diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
> index 3e404e51ec64..771b6b10d519 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,10 +161,8 @@ static int populate_ruleset(const char *const env_var, const int ruleset_fd,
>   	LANDLOCK_ACCESS_FS_MAKE_FIFO | \
>   	LANDLOCK_ACCESS_FS_MAKE_BLOCK | \
>   	LANDLOCK_ACCESS_FS_MAKE_SYM | \
> -	LANDLOCK_ACCESS_FS_REFER)
> -
> -#define ACCESS_ABI_2 ( \
> -	LANDLOCK_ACCESS_FS_REFER)
> +	LANDLOCK_ACCESS_FS_REFER | \
> +	LANDLOCK_ACCESS_FS_TRUNCATE)
>   
>   /* clang-format on */
>   
> @@ -226,11 +225,17 @@ int main(const int argc, char *const argv[], char *const *const envp)
>   		return 1;
>   	}
>   	/* Best-effort security. */
> -	if (abi < 2) {
> -		ruleset_attr.handled_access_fs &= ~ACCESS_ABI_2;
> -		access_fs_ro &= ~ACCESS_ABI_2;
> -		access_fs_rw &= ~ACCESS_ABI_2;
> +	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;
>   	}
> +	access_fs_ro &= ruleset_attr.handled_access_fs;
> +	access_fs_rw &= ruleset_attr.handled_access_fs;
>   
>   	ruleset_fd =
>   		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 5/5] landlock: Document Landlock's file truncation support
  2022-09-12 17:47       ` Mickaël Salaün
@ 2022-09-12 19:05         ` Günther Noack
  0 siblings, 0 replies; 39+ messages in thread
From: Günther Noack @ 2022-09-12 19:05 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Mon, Sep 12, 2022 at 07:47:11PM +0200, Mickaël Salaün wrote:
> On 12/09/2022 17:46, Günther Noack wrote:
> > On Fri, Sep 09, 2022 at 03:51:35PM +0200, Mickaël Salaün wrote:
> > > On 08/09/2022 21:58, Günther Noack wrote:
> > > > Use the LANDLOCK_ACCESS_FS_TRUNCATE flag in the tutorial.
> > > > 
> > > > Adapt the backwards compatibility example and discussion to remove the
> > > > truncation flag where needed.
> > > > 
> > > > Point out potential surprising behaviour related to truncate.
> > > > 
> > > > Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> > > > ---
> > > >    Documentation/userspace-api/landlock.rst | 62 +++++++++++++++++++++---
> > > >    1 file changed, 54 insertions(+), 8 deletions(-)
> > > > 
> > > > diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
> > > > index b8ea59493964..57802fd1e09b 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: May 2022
> > > > +:Date: September 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,26 @@ 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) {
> > > > -        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
> > > > +    switch (abi) {
> > > > +    case -1:
> > > > +            perror("The running kernel does not enable to use Landlock");
> > > > +            return 1;
> > > > +    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 +138,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.
> > > 
> > > This fix is correct, but it should not be part of this series. FYI, I have a
> > > patch almost ready to fix some documentation style issues. Please remove
> > > this hunk for the next series. I'll deal with the merge conflicts if any.
> > 
> > Can you please clarify what part of it should not be part of this
> > series?
> 
> My mistake, I guess I was reviewing something else… I was thinking about
> style changes, but it is not the case here. Using "``" is correct.
> 
> 
> > 
> > In this hunk, I've started using double backquote, but I've also
> > changed the meaning of the sentence slightly so that it is still
> > correct when the truncate right is introduced.
> > 
> > It is still correct that the backwards compatibility check is not
> > required because LANDLOCK_ACCESS_FS_REFER is not allowed by any rule.
> > But with the new truncate flag, LANDLOCK_ACCESS_FS_TRUNCATE may also
> > not be allowed by any rule so that we can skip this check.
> > 
> > Should I remove this hunk entirely?
> 
> Keep your changes, it's better like this.

Thanks, reverted that part then.

—Günther

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 4/5] samples/landlock: Extend sample tool to support LANDLOCK_ACCESS_FS_TRUNCATE
  2022-09-12 19:05   ` Mickaël Salaün
@ 2022-09-12 19:07     ` Günther Noack
  0 siblings, 0 replies; 39+ messages in thread
From: Günther Noack @ 2022-09-12 19:07 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Mon, Sep 12, 2022 at 09:05:03PM +0200, Mickaël Salaün wrote:
> On 08/09/2022 21:58, Günther Noack wrote:
> > Update the sandboxer sample to restrict truncate actions. This is
> > automatically enabled by default if the running kernel supports
> > LANDLOCK_ACCESS_FS_TRUNCATE, expect for the paths listed in the
> 
> except for

Fixed, good catch!

-Günther

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 5/5] landlock: Document Landlock's file truncation support
  2022-09-08 19:58 ` [PATCH v6 5/5] landlock: Document Landlock's file truncation support Günther Noack
  2022-09-09 13:51   ` Mickaël Salaün
@ 2022-09-12 19:15   ` Mickaël Salaün
  2022-09-23 11:30     ` Günther Noack
  1 sibling, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-12 19:15 UTC (permalink / raw)
  To: Günther Noack, linux-security-module
  Cc: James Morris, Paul Moore, Serge E . Hallyn, linux-fsdevel,
	Konstantin Meskhidze



On 08/09/2022 21:58, Günther Noack wrote:
> Use the LANDLOCK_ACCESS_FS_TRUNCATE flag in the tutorial.
> 
> Adapt the backwards compatibility example and discussion to remove the
> truncation flag where needed.
> 
> Point out potential surprising behaviour related to truncate.
> 
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
>   Documentation/userspace-api/landlock.rst | 62 +++++++++++++++++++++---
>   1 file changed, 54 insertions(+), 8 deletions(-)
> 
> diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
> index b8ea59493964..57802fd1e09b 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: May 2022
> +:Date: September 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,26 @@ 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) {
> -        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
> +    switch (abi) {
> +    case -1:
> +            perror("The running kernel does not enable to use Landlock");
> +            return 1;

I think it would be easier to understand to explicitly check for abi < 0 
in a dedicated block as in the sample, instead of case -1, and return 0 
(instead of 1) with a comment to inform that Landlock is not handled but 
it is OK (expected error).


> +    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.

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/5] landlock: Support file truncation
  2022-09-08 19:58 ` [PATCH v6 2/5] landlock: Support file truncation Günther Noack
  2022-09-09 13:51   ` Mickaël Salaün
@ 2022-09-12 19:41   ` Mickaël Salaün
  2022-09-23 11:21     ` Günther Noack
  1 sibling, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-12 19:41 UTC (permalink / raw)
  To: Günther Noack, linux-security-module
  Cc: James Morris, Paul Moore, Serge E . Hallyn, linux-fsdevel,
	Konstantin Meskhidze


On 08/09/2022 21:58, Günther Noack wrote:
> Introduce the LANDLOCK_ACCESS_FS_TRUNCATE flag for file truncation.

[...]

> @@ -761,6 +762,47 @@ static bool collect_domain_accesses(
>   	return ret;
>   }
>   
> +/**
> + * get_path_access_rights - Returns the subset of rights in access_request
> + * which are permitted for the given path.
> + *
> + * @domain: The domain that defines the current restrictions.
> + * @path: The path to get access rights for.
> + * @access_request: The rights we are interested in.
> + *
> + * Returns: The access mask of the rights that are permitted on the given path,
> + * which are also a subset of access_request (to save some calculation time).
> + */
> +static inline access_mask_t
> +get_path_access_rights(const struct landlock_ruleset *const domain,
> +		       const struct path *const path,
> +		       access_mask_t access_request)
> +{
> +	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
> +	unsigned long access_bit;
> +	unsigned long access_req;
> +
> +	init_layer_masks(domain, access_request, &layer_masks);
> +	if (!check_access_path_dual(domain, path, access_request, &layer_masks,
> +				    NULL, 0, NULL, NULL)) {
> +		/*
> +		 * Return immediately for successful accesses and for cases
> +		 * where everything is permitted because the path belongs to an
> +		 * internal filesystem.
> +		 */
> +		return access_request;
> +	}
> +
> +	access_req = access_request;
> +	for_each_set_bit(access_bit, &access_req, ARRAY_SIZE(layer_masks)) {
> +		if (layer_masks[access_bit]) {
> +			/* If any layer vetoed the access right, remove it. */
> +			access_request &= ~BIT_ULL(access_bit);
> +		}
> +	}

This seems to be redundant with the value returned by 
init_layer_masks(), which should be passed to check_access_path_dual() 
to avoid useless path walk.

This function is pretty similar to check_access_path(). Can't you change 
it to use an access_mask_t pointer and get almost the same thing?


> +	return access_request;
> +}
> +
>   /**
>    * current_check_refer_path - Check if a rename or link action is allowed
>    *
> @@ -1142,6 +1184,11 @@ 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)
> @@ -1159,22 +1206,55 @@ static inline access_mask_t get_file_access(const struct file *const file)
>   	/* __FMODE_EXEC is indeed part of f_flags, not f_mode. */
>   	if (file->f_flags & __FMODE_EXEC)
>   		access |= LANDLOCK_ACCESS_FS_EXECUTE;
> +
>   	return access;
>   }
>   
>   static int hook_file_open(struct file *const file)
>   {
> +	access_mask_t access_req, access_rights;

"access_request" is used for access_mask_t, and "access_req" for 
unsigned int. I'd like to stick to this convention.


> +	const access_mask_t optional_rights = LANDLOCK_ACCESS_FS_TRUNCATE;

You use "rights" often and I'm having some trouble to find a rational 
for that (compared to "access")…


>   	const struct landlock_ruleset *const dom =
>   		landlock_get_current_domain();
>   
> -	if (!dom)
> +	if (!dom) {
> +		/* Grant all rights. */
> +		landlock_file(file)->rights = LANDLOCK_MASK_ACCESS_FS;
>   		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
>   	 * evolution.
>   	 */
> -	return check_access_path(dom, &file->f_path, get_file_access(file));
> +	access_req = get_file_access(file);
> +	access_rights = get_path_access_rights(dom, &file->f_path,
> +					       access_req | optional_rights);
> +	if (access_req & ~access_rights)
> +		return -EACCES;

We should add a test to make sure this (optional_rights) logic is 
correct (and doesn't change), with a matrix of cases involving a ruleset 
handling either FS_WRITE, FS_TRUNCATE or both. This should be easy to do 
with test variants.


> +
> +	/*
> +	 * 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)->rights = access_rights;
> +
> +	return 0;
> +}

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 3/5] selftests/landlock: Selftests for file truncation support
  2022-09-08 19:58 ` [PATCH v6 3/5] selftests/landlock: Selftests for file truncation support Günther Noack
@ 2022-09-16 17:05   ` Mickaël Salaün
  2022-09-23 17:50     ` Günther Noack
  0 siblings, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-16 17:05 UTC (permalink / raw)
  To: Günther Noack, linux-security-module
  Cc: James Morris, Paul Moore, Serge E . Hallyn, linux-fsdevel,
	Konstantin Meskhidze

I'd like to have tests similar to base_test.c:ruleset_fd_transfer to 
check ftruncate with different kind of file descriptors and 
not-sandboxed processes. That would require some code refactoring to 
reuse the FD passing code.


On 08/09/2022 21:58, Günther Noack wrote:
> These tests exercise the following truncation operations:
> 
> * truncate() (truncate by path)
> * ftruncate() (truncate by file descriptor)
> * open with the O_TRUNC flag
> * special case: creat(), which is open with O_CREAT|O_WRONLY|O_TRUNC.
> 
> in the following scenarios:
> 
> * Files with read, write and truncate rights.
> * Files with read and truncate rights.
> * Files with the truncate right.
> * Files without the truncate right.
> 
> In particular, the following scenarios are enforced with the test:
> 
> * open() with O_TRUNC requires the truncate right, if it truncates a file.
>    open() already checks security_path_truncate() in this case,
>    and it required no additional check in the Landlock LSM's file_open hook.
> * creat() requires the truncate right
>    when called with an existing filename.
> * creat() does *not* require the truncate right
>    when it's creating a new file.
> * ftruncate() requires that the file was opened by a thread that had
>    the truncate right for the file at the time of open(). (The rights
>    are carried along with the opened file.)
> 
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
>   tools/testing/selftests/landlock/fs_test.c | 280 +++++++++++++++++++++
>   1 file changed, 280 insertions(+)
> 
> diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
> index 87b28d14a1aa..ddc8c7e57e86 100644
> --- a/tools/testing/selftests/landlock/fs_test.c
> +++ b/tools/testing/selftests/landlock/fs_test.c
> @@ -58,6 +58,7 @@ static const char file1_s2d3[] = TMP_DIR "/s2d1/s2d2/s2d3/f1";
>   static const char file2_s2d3[] = TMP_DIR "/s2d1/s2d2/s2d3/f2";
>   
>   static const char dir_s3d1[] = TMP_DIR "/s3d1";
> +static const char file1_s3d1[] = TMP_DIR "/s3d1/f1";
>   /* dir_s3d2 is a mount point. */
>   static const char dir_s3d2[] = TMP_DIR "/s3d1/s3d2";
>   static const char dir_s3d3[] = TMP_DIR "/s3d1/s3d2/s3d3";
> @@ -83,6 +84,7 @@ static const char dir_s3d3[] = TMP_DIR "/s3d1/s3d2/s3d3";
>    * │           ├── f1
>    * │           └── f2
>    * └── s3d1
> + *     ├── f1
>    *     └── s3d2
>    *         └── s3d3
>    */
> @@ -208,6 +210,7 @@ static void create_layout1(struct __test_metadata *const _metadata)
>   	create_file(_metadata, file1_s2d3);
>   	create_file(_metadata, file2_s2d3);
>   
> +	create_file(_metadata, file1_s3d1);
>   	create_directory(_metadata, dir_s3d2);
>   	set_cap(_metadata, CAP_SYS_ADMIN);
>   	ASSERT_EQ(0, mount("tmp", dir_s3d2, "tmpfs", 0, "size=4m,mode=700"));
> @@ -230,6 +233,7 @@ static void remove_layout1(struct __test_metadata *const _metadata)
>   	EXPECT_EQ(0, remove_path(file1_s2d2));
>   	EXPECT_EQ(0, remove_path(file1_s2d1));
>   
> +	EXPECT_EQ(0, remove_path(file1_s3d1));
>   	EXPECT_EQ(0, remove_path(dir_s3d3));
>   	set_cap(_metadata, CAP_SYS_ADMIN);
>   	umount(dir_s3d2);
> @@ -3158,6 +3162,282 @@ TEST_F_FORK(layout1, proc_pipe)
>   	ASSERT_EQ(0, close(pipe_fds[1]));
>   }
>   
> +/* Invokes truncate(2) and returns its errno or 0. */
> +static int test_truncate(const char *const path)
> +{
> +	if (truncate(path, 10) < 0)
> +		return errno;
> +	return 0;
> +}
> +
> +/*
> + * Invokes creat(2) and returns its errno or 0.
> + * Closes the opened file descriptor on success.
> + */
> +static int test_creat(const char *const path)
> +{
> +	int fd = creat(path, 0600);
> +
> +	if (fd < 0)
> +		return errno;
> +
> +	/*
> +	 * Mixing error codes from close(2) and creat(2) should not lead to any
> +	 * (access type) confusion for this test.
> +	 */
> +	if (close(fd) < 0)
> +		return errno;
> +	return 0;
> +}
> +
> +/*
> + * Exercises file truncation when it's not restricted,
> + * as it was the case before LANDLOCK_ACCESS_FS_TRUNCATE existed.
> + */
> +TEST_F_FORK(layout1, truncate_unhandled)
> +{
> +	const char *const file_r = file1_s1d1;
> +	const char *const file_w = file2_s1d1;
> +	const char *const file_none = file1_s1d2;
> +	const struct rule rules[] = {
> +		{
> +			.path = file_r,
> +			.access = LANDLOCK_ACCESS_FS_READ_FILE,
> +		},
> +		{
> +			.path = file_w,
> +			.access = LANDLOCK_ACCESS_FS_WRITE_FILE,
> +		},
> +		/* Implicitly: No rights for file_none. */
> +		{},
> +	};
> +
> +	const __u64 handled = LANDLOCK_ACCESS_FS_READ_FILE |
> +			      LANDLOCK_ACCESS_FS_WRITE_FILE;
> +	int ruleset_fd;
> +
> +	/* Enable Landlock. */
> +	ruleset_fd = create_ruleset(_metadata, handled, rules);
> +
> +	ASSERT_LE(0, ruleset_fd);
> +	enforce_ruleset(_metadata, ruleset_fd);
> +	ASSERT_EQ(0, close(ruleset_fd));
> +
> +	/*
> +	 * Checks read right: truncate and open with O_TRUNC work, unless the
> +	 * file is attempted to be opened for writing.
> +	 */
> +	EXPECT_EQ(0, test_truncate(file_r));
> +	EXPECT_EQ(0, test_open(file_r, O_RDONLY | O_TRUNC));
> +	EXPECT_EQ(EACCES, test_open(file_r, O_WRONLY | O_TRUNC));
> +	EXPECT_EQ(EACCES, test_creat(file_r));
> +
> +	/*
> +	 * Checks write right: truncate and open with O_TRUNC work, unless the
> +	 * file is attempted to be opened for reading.
> +	 */
> +	EXPECT_EQ(0, test_truncate(file_w));
> +	EXPECT_EQ(EACCES, test_open(file_w, O_RDONLY | O_TRUNC));
> +	EXPECT_EQ(0, test_open(file_w, O_WRONLY | O_TRUNC));
> +	EXPECT_EQ(0, test_creat(file_w));
> +
> +	/*
> +	 * Checks "no rights" case: truncate works but all open attempts fail,
> +	 * including creat.
> +	 */
> +	EXPECT_EQ(0, test_truncate(file_none));
> +	EXPECT_EQ(EACCES, test_open(file_none, O_RDONLY | O_TRUNC));
> +	EXPECT_EQ(EACCES, test_open(file_none, O_WRONLY | O_TRUNC));
> +	EXPECT_EQ(EACCES, test_creat(file_none));
> +}
> +
> +TEST_F_FORK(layout1, truncate)
> +{
> +	const char *const file_rwt = file1_s1d1;
> +	const char *const file_rw = file2_s1d1;
> +	const char *const file_rt = file1_s1d2;
> +	const char *const file_t = file2_s1d2;
> +	const char *const file_none = file1_s1d3;
> +	const char *const dir_t = dir_s2d1;
> +	const char *const file_in_dir_t = file1_s2d1;
> +	const char *const dir_w = dir_s3d1;
> +	const char *const file_in_dir_w = file1_s3d1;
> +	int file_rwt_fd, file_rw_fd;

These variables are unused now.


> +	const struct rule rules[] = {
> +		{
> +			.path = file_rwt,
> +			.access = LANDLOCK_ACCESS_FS_READ_FILE |
> +				  LANDLOCK_ACCESS_FS_WRITE_FILE |
> +				  LANDLOCK_ACCESS_FS_TRUNCATE,
> +		},
> +		{
> +			.path = file_rw,
> +			.access = LANDLOCK_ACCESS_FS_READ_FILE |
> +				  LANDLOCK_ACCESS_FS_WRITE_FILE,
> +		},
> +		{
> +			.path = file_rt,
> +			.access = LANDLOCK_ACCESS_FS_READ_FILE |
> +				  LANDLOCK_ACCESS_FS_TRUNCATE,
> +		},
> +		{
> +			.path = file_t,
> +			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
> +		},
> +		/* Implicitly: No access rights for file_none. */
> +		{
> +			.path = dir_t,
> +			.access = LANDLOCK_ACCESS_FS_TRUNCATE,
> +		},
> +		{
> +			.path = dir_w,
> +			.access = LANDLOCK_ACCESS_FS_WRITE_FILE,
> +		},
> +		{},
> +	};
> +	const __u64 handled = LANDLOCK_ACCESS_FS_READ_FILE |
> +			      LANDLOCK_ACCESS_FS_WRITE_FILE |
> +			      LANDLOCK_ACCESS_FS_TRUNCATE;
> +	int ruleset_fd;
> +
> +	/* Enable Landlock. */
> +	ruleset_fd = create_ruleset(_metadata, handled, rules);
> +
> +	ASSERT_LE(0, ruleset_fd);
> +	enforce_ruleset(_metadata, ruleset_fd);
> +	ASSERT_EQ(0, close(ruleset_fd));
> +
> +	/* Checks read, write and truncate rights: truncation works. */
> +	EXPECT_EQ(0, test_truncate(file_rwt));
> +	EXPECT_EQ(0, test_open(file_rwt, O_RDONLY | O_TRUNC));
> +	EXPECT_EQ(0, test_open(file_rwt, O_WRONLY | O_TRUNC));
> +
> +	/* Checks read and write rights: no truncate variant works. */
> +	EXPECT_EQ(EACCES, test_truncate(file_rw));
> +	EXPECT_EQ(EACCES, test_open(file_rw, O_RDONLY | O_TRUNC));
> +	EXPECT_EQ(EACCES, test_open(file_rw, O_WRONLY | O_TRUNC));
> +
> +	/*
> +	 * Checks read and truncate rights: truncation works.
> +	 *
> +	 * Note: Files can get truncated using open() even with O_RDONLY.
> +	 */
> +	EXPECT_EQ(0, test_truncate(file_rt));
> +	EXPECT_EQ(0, test_open(file_rt, O_RDONLY | O_TRUNC));
> +	EXPECT_EQ(EACCES, test_open(file_rt, O_WRONLY | O_TRUNC));
> +
> +	/* Checks truncate right: truncate works, but can't open file. */
> +	EXPECT_EQ(0, test_truncate(file_t));
> +	EXPECT_EQ(EACCES, test_open(file_t, O_RDONLY | O_TRUNC));
> +	EXPECT_EQ(EACCES, test_open(file_t, O_WRONLY | O_TRUNC));
> +
> +	/* Checks "no rights" case: No form of truncation works. */
> +	EXPECT_EQ(EACCES, test_truncate(file_none));
> +	EXPECT_EQ(EACCES, test_open(file_none, O_RDONLY | O_TRUNC));
> +	EXPECT_EQ(EACCES, test_open(file_none, O_WRONLY | O_TRUNC));
> +
> +	/*
> +	 * Checks truncate right on directory: truncate works on contained
> +	 * files.
> +	 */
> +	EXPECT_EQ(0, test_truncate(file_in_dir_t));
> +	EXPECT_EQ(EACCES, test_open(file_in_dir_t, O_RDONLY | O_TRUNC));
> +	EXPECT_EQ(EACCES, test_open(file_in_dir_t, O_WRONLY | O_TRUNC));
> +
> +	/*
> +	 * Checks creat in dir_w: This requires the truncate right when
> +	 * overwriting an existing file, but does not require it when the file
> +	 * is new.
> +	 */
> +	EXPECT_EQ(EACCES, test_creat(file_in_dir_w));
> +
> +	ASSERT_EQ(0, unlink(file_in_dir_w));
> +	EXPECT_EQ(0, test_creat(file_in_dir_w));
> +}
> +
> +static void landlock_single_path(struct __test_metadata *const _metadata,
> +				 const char *const path, __u64 handled,
> +				 __u64 permitted)
> +{
> +	const struct rule rules[] = {
> +		{
> +			.path = path,
> +			.access = permitted,
> +		},
> +		{},
> +	};
> +	int ruleset_fd = create_ruleset(_metadata, handled, rules);
> +
> +	ASSERT_LE(0, ruleset_fd);
> +
> +	enforce_ruleset(_metadata, ruleset_fd);
> +
> +	ASSERT_EQ(0, close(ruleset_fd));
> +}
> +
> +/* Invokes ftruncate(2) and returns its errno or 0. */
> +static int test_ftruncate(int fd)
> +{
> +	if (ftruncate(fd, 10) < 0)
> +		return errno;
> +	return 0;
> +}
> +
> +TEST_F_FORK(layout1, ftruncate)

Great!

> +{
> +	/*
> +	 * This test opens a new file descriptor at different stages of
> +	 * Landlock restriction:
> +	 *
> +	 * without restriction:                    ftruncate works
> +	 * something else but truncate restricted: ftruncate works
> +	 * truncate restricted and permitted:      ftruncate works
> +	 * truncate restricted and not permitted:  ftruncate fails
> +	 *
> +	 * Whether this works or not is expected to depend on the time when the
> +	 * FD was opened, not to depend on the time when ftruncate() was
> +	 * called.
> +	 */
> +	const char *const path = file1_s1d1;
> +	int fd0, fd1, fd2, fd3;

You can rename them fd_layer0, fd_layer1…


> +
> +	fd0 = open(path, O_WRONLY);
> +	EXPECT_EQ(0, test_ftruncate(fd0));
> +
> +	landlock_single_path(_metadata, path,
> +			     LANDLOCK_ACCESS_FS_READ_FILE |
> +				     LANDLOCK_ACCESS_FS_WRITE_FILE,
> +			     LANDLOCK_ACCESS_FS_WRITE_FILE);

I'd prefer to follow the current way to write rule layers: write all 
struct rule at first and then call each enforcement steps. It is a bit 
more verbose but easier to understand errors. The list of test_ftruncate 
checks are straightforward to follow.


> +
> +	fd1 = open(path, O_WRONLY);
> +	EXPECT_EQ(0, test_ftruncate(fd0));
> +	EXPECT_EQ(0, test_ftruncate(fd1));
> +
> +	landlock_single_path(_metadata, path, LANDLOCK_ACCESS_FS_TRUNCATE,
> +			     LANDLOCK_ACCESS_FS_TRUNCATE);
> +
> +	fd2 = open(path, O_WRONLY);
> +	EXPECT_EQ(0, test_ftruncate(fd0));
> +	EXPECT_EQ(0, test_ftruncate(fd1));
> +	EXPECT_EQ(0, test_ftruncate(fd2));
> +
> +	landlock_single_path(_metadata, path,
> +			     LANDLOCK_ACCESS_FS_TRUNCATE |
> +				     LANDLOCK_ACCESS_FS_WRITE_FILE,
> +			     LANDLOCK_ACCESS_FS_WRITE_FILE);
> +
> +	fd3 = open(path, O_WRONLY);
> +	EXPECT_EQ(0, test_ftruncate(fd0));
> +	EXPECT_EQ(0, test_ftruncate(fd1));
> +	EXPECT_EQ(0, test_ftruncate(fd2));
> +	EXPECT_EQ(EACCES, test_ftruncate(fd3));
> +
> +	ASSERT_EQ(0, close(fd0));
> +	ASSERT_EQ(0, close(fd1));
> +	ASSERT_EQ(0, close(fd2));
> +	ASSERT_EQ(0, close(fd3));
> +}
> +
>   /* clang-format off */
>   FIXTURE(layout1_bind) {};
>   /* clang-format on */

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook
  2022-09-08 20:28   ` Günther Noack
@ 2022-09-16 17:30     ` Mickaël Salaün
  2022-09-26 16:07       ` Günther Noack
  0 siblings, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-16 17:30 UTC (permalink / raw)
  To: Günther Noack, Namjae Jeon, David Howells, Christian Brauner
  Cc: James Morris, Paul Moore, Serge E . Hallyn, linux-fsdevel,
	Konstantin Meskhidze, linux-security-module

We may indeed need to change fs/open.c:vfs_truncate() because of these 
different call sites. I'm not sure how these subsystems work though.


On 08/09/2022 22:28, Günther Noack wrote:
> Adding Namjae Jeon and David Howells as authors of the respective
> files in fs/ksmbd and fs/cachefiles -- do you happen to know whether
> these vfs_truncate() calls are using 'struct file's that are opened by
> normal userspace processes, where LSM policies may apply?
> 
> P.S. In this patch I have looked for all places where the
> security_path_truncate() hook was called, to see which of these should
> rather use security_file_truncate() (and I made sure that it does the
> same thing for all the LSMs that use it).
> 
> I'm confident that this does the right thing when truncate() or
> ftruncate() are called from userspace, but one of the places that
> still calls the path-based hook is vfs_truncate(), and this is called
> from more places in the kernel than just from userspace:
> 
> init/initramfs.c
> 387:				vfs_truncate(&wfile->f_path, body_len);
> 
> security/keys/big_key.c
> 172:		vfs_truncate(&payload->path, 0);
> 
> fs/cachefiles/interface.c
> 242:		ret = vfs_truncate(&file->f_path, dio_size);
> 
> fs/cachefiles/namei.c
> 497:			ret = vfs_truncate(&path, ni_size); >
> fs/ksmbd/smb2pdu.c
> 2350:	int rc = vfs_truncate(path, 0);
> 
> fs/ksmbd/vfs.c
> 874:	err = vfs_truncate(&filp->f_path, size);
> 
> I suspect that these are benign but am not familiar with all of these
> corners of the codebase. -- The question is: Some of these call
> vfs_truncate() on the f_path of an existing struct file -- should
> these rather be calling the security_file_truncate() than the
> security_path_truncate() hook to authorize the truncation?
> 
> Specifically, I think:
> 
> * initramfs happens at system startup and LSMs should not interfere at
>    this point yet
> * security/keys does not use an opened struct file, so calling the
>    path-based hook through vfs_truncate() is correct
> * fs/cachefiles and fs/ksmbd use the file system from the kernel to
>    expose it as another file system (in a cached form for cachefiles,
>    and over the network for ksmbd). I suspect that these file systems
>    are not handling 'struct file's which are opened in contexts where a
>    LSM applies? It that a reasonable assumption?

I think you're right but I have some doubts about the cachefiles subsystem.
I don't know how ksmb deals with these file descriptors but changing 
such call sites (where there is a struct file) could improve API 
consistency though.
Any though?


> 
> Thanks,
> Günther
> 
> On Thu, Sep 08, 2022 at 09:58:01PM +0200, Günther Noack wrote:
>> Like path_truncate, the file_truncate hook also restricts file
>> truncation, but is called in the cases where truncation is attempted
>> on an already-opened file.
>>
>> This is required in a subsequent commit to handle ftruncate()
>> operations differently to truncate() operations.
>>
>> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
>> ---
>>   fs/namei.c                    |  6 +++---
>>   fs/open.c                     |  4 ++--
>>   include/linux/lsm_hook_defs.h |  1 +
>>   include/linux/security.h      |  6 ++++++
>>   security/apparmor/lsm.c       |  6 ++++++
>>   security/security.c           |  5 +++++
>>   security/tomoyo/tomoyo.c      | 13 +++++++++++++
>>   7 files changed, 36 insertions(+), 5 deletions(-)
>>
>> diff --git a/fs/namei.c b/fs/namei.c
>> index 53b4bc094db2..52105873d1f8 100644
>> --- a/fs/namei.c
>> +++ b/fs/namei.c
>> @@ -53,8 +53,8 @@
>>    * The new code replaces the old recursive symlink resolution with
>>    * an iterative one (in case of non-nested symlink chains).  It does
>>    * this with calls to <fs>_follow_link().
>> - * As a side effect, dir_namei(), _namei() and follow_link() are now
>> - * replaced with a single function lookup_dentry() that can handle all
>> + * As a side effect, dir_namei(), _namei() and follow_link() are now
>> + * replaced with a single function lookup_dentry() that can handle all
>>    * the special cases of the former code.
>>    *
>>    * With the new dcache, the pathname is stored at each inode, at least as
>> @@ -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 8a813fa5ca56..0831433e493a 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);
>> @@ -1271,7 +1271,7 @@ struct file *filp_open(const char *filename, int flags, umode_t mode)
>>   {
>>   	struct filename *name = getname_kernel(filename);
>>   	struct file *file = ERR_CAST(name);
>> -
>> +
>>   	if (!IS_ERR(name)) {
>>   		file = file_open_name(name, flags, mode);
>>   		putname(name);
>> diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
>> index 60fff133c0b1..dee35ab253ba 100644
>> --- a/include/linux/lsm_hook_defs.h
>> +++ b/include/linux/lsm_hook_defs.h
>> @@ -177,6 +177,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/security.h b/include/linux/security.h
>> index 7bd0c490703d..f80b23382dd9 100644
>> --- a/include/linux/security.h
>> +++ b/include/linux/security.h
>> @@ -394,6 +394,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);
>> @@ -1011,6 +1012,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/security/apparmor/lsm.c b/security/apparmor/lsm.c
>> index e29cade7b662..98ecb7f221b8 100644
>> --- a/security/apparmor/lsm.c
>> +++ b/security/apparmor/lsm.c
>> @@ -329,6 +329,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)
>>   {
>> @@ -1232,6 +1237,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/security.c b/security/security.c
>> index 4b95de24bc8d..e491120c48ba 100644
>> --- a/security/security.c
>> +++ b/security/security.c
>> @@ -1210,6 +1210,11 @@ int security_path_truncate(const struct path *path)
>>   	return call_int_hook(path_truncate, 0, path);
>>   }
>>
>> +int security_file_truncate(struct file *file)
>> +{
>> +	return call_int_hook(file_truncate, 0, file);
>> +}
>> +
>>   int security_path_chmod(const struct path *path, umode_t mode)
>>   {
>>   	if (unlikely(IS_PRIVATE(d_backing_inode(path->dentry))))
>> 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
>> @@ -134,6 +134,18 @@ static int tomoyo_path_truncate(const struct path *path)
>>   	return tomoyo_path_perm(TOMOYO_TYPE_TRUNCATE, path, NULL);
>>   }
>>
>> +/**
>> + * 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().
>>    *
>> @@ -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),
>> --
>> 2.37.3
>>
> 
> --

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/5] landlock: Support file truncation
  2022-09-12 19:41   ` Mickaël Salaün
@ 2022-09-23 11:21     ` Günther Noack
  2022-09-23 20:53       ` Mickaël Salaün
  0 siblings, 1 reply; 39+ messages in thread
From: Günther Noack @ 2022-09-23 11:21 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Mon, Sep 12, 2022 at 09:41:32PM +0200, Mickaël Salaün wrote:
> 
> On 08/09/2022 21:58, Günther Noack wrote:
> > Introduce the LANDLOCK_ACCESS_FS_TRUNCATE flag for file truncation.
> 
> [...]
> 
> > @@ -761,6 +762,47 @@ static bool collect_domain_accesses(
> >   	return ret;
> >   }
> > +/**
> > + * get_path_access_rights - Returns the subset of rights in access_request
> > + * which are permitted for the given path.
> > + *
> > + * @domain: The domain that defines the current restrictions.
> > + * @path: The path to get access rights for.
> > + * @access_request: The rights we are interested in.
> > + *
> > + * Returns: The access mask of the rights that are permitted on the given path,
> > + * which are also a subset of access_request (to save some calculation time).
> > + */
> > +static inline access_mask_t
> > +get_path_access_rights(const struct landlock_ruleset *const domain,
> > +		       const struct path *const path,
> > +		       access_mask_t access_request)
> > +{
> > +	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
> > +	unsigned long access_bit;
> > +	unsigned long access_req;
> > +
> > +	init_layer_masks(domain, access_request, &layer_masks);
> > +	if (!check_access_path_dual(domain, path, access_request, &layer_masks,
> > +				    NULL, 0, NULL, NULL)) {
> > +		/*
> > +		 * Return immediately for successful accesses and for cases
> > +		 * where everything is permitted because the path belongs to an
> > +		 * internal filesystem.
> > +		 */
> > +		return access_request;
> > +	}
> > +
> > +	access_req = access_request;
> > +	for_each_set_bit(access_bit, &access_req, ARRAY_SIZE(layer_masks)) {
> > +		if (layer_masks[access_bit]) {
> > +			/* If any layer vetoed the access right, remove it. */
> > +			access_request &= ~BIT_ULL(access_bit);
> > +		}
> > +	}
> 
> This seems to be redundant with the value returned by init_layer_masks(),
> which should be passed to check_access_path_dual() to avoid useless path
> walk.

True, I'll use the result of init_layer_masks() to feed it back to
check_access_path_dual() to avoid a bit of computation.

Like this:

        effective_access_request =
		init_layer_masks(domain, access_request, &layer_masks);
	if (!check_access_path_dual(domain, path, effective_access_request,
	    &layer_masks, NULL, 0, NULL, NULL)) {
		// ...
	}

Overall, the approach here is:

* Initialize the layer_masks, so that it has a bit set for every
  access right in access_request and layer where that access right is
  handled.

* check_access_path_dual() with only the first few parameters -- this
  will clear all the bits in layer masks which are actually permitted
  according to the individual rules.

  As a special case, this *may* return 0 immediately, in which case we
  can (a) save a bit of calculation in the loop below and (b) we might
  be in the case where access is permitted because it's a file from a
  special file system (even though not all bits are cleared). If
  check_access_path_dual() returns 0, we return the full requested
  access_request that we received as input.

* In the loop below, if there are any bits left in layer_masks, those
  are rights which are not permitted for the given path. We remove
  these from access_request and return the modified access_request.


> This function is pretty similar to check_access_path(). Can't you change it
> to use an access_mask_t pointer and get almost the same thing?

I'm shying away from this approach. Many of the existing different use
cases are already realized by "doing if checks deep down". I think it
would make the code more understandable if we managed to model these
differences between use cases already at the layer of function calls.
(This is particularly true for check_access_path_dual(), where in
order to find out how the "single" case works, you need to disentangle
to a large extent how the much more complicated dual case works.)

If you want to unify these two functions, what do you think of the
approach of just using get_path_access_rights() instead of
check_access_path()?

Basically, it would turn

return check_access_path(dom, path, access_request);

into

if (get_path_access_rights(dom, path, access_request) == access_request)
	return 0;
return -EACCES;

This is slightly more verbose in the places where it's called, but it
would be more orthogonal, and it would also clarify that -EACCES is
the only possible error in the "single" path walk case.

Let me know what you think.

> > +	return access_request;
> > +}
> > +
> >   /**
> >    * current_check_refer_path - Check if a rename or link action is allowed
> >    *
> > @@ -1142,6 +1184,11 @@ 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)
> > @@ -1159,22 +1206,55 @@ static inline access_mask_t get_file_access(const struct file *const file)
> >   	/* __FMODE_EXEC is indeed part of f_flags, not f_mode. */
> >   	if (file->f_flags & __FMODE_EXEC)
> >   		access |= LANDLOCK_ACCESS_FS_EXECUTE;
> > +
> >   	return access;
> >   }
> >   static int hook_file_open(struct file *const file)
> >   {
> > +	access_mask_t access_req, access_rights;
> 
> "access_request" is used for access_mask_t, and "access_req" for unsigned
> int. I'd like to stick to this convention.

Done.

> > +	const access_mask_t optional_rights = LANDLOCK_ACCESS_FS_TRUNCATE;
> 
> You use "rights" often and I'm having some trouble to find a rational for
> that (compared to "access")…

Done. Didn't realize you already had a different convention here.

I'm renaming get_path_access_rights() to get_path_access() as well
then (and I'll rename get_file_access() to
get_required_file_open_access() - that's more verbose, but it sounded
too similar to get_path_access(), and it might be better to clarify
that this is a helper for the file_open hook). Does that sound
reasonable?


> >   	const struct landlock_ruleset *const dom =
> >   		landlock_get_current_domain();
> > -	if (!dom)
> > +	if (!dom) {
> > +		/* Grant all rights. */
> > +		landlock_file(file)->rights = LANDLOCK_MASK_ACCESS_FS;
> >   		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
> >   	 * evolution.
> >   	 */
> > -	return check_access_path(dom, &file->f_path, get_file_access(file));
> > +	access_req = get_file_access(file);
> > +	access_rights = get_path_access_rights(dom, &file->f_path,
> > +					       access_req | optional_rights);
> > +	if (access_req & ~access_rights)
> > +		return -EACCES;
> 
> We should add a test to make sure this (optional_rights) logic is correct
> (and doesn't change), with a matrix of cases involving a ruleset handling
> either FS_WRITE, FS_TRUNCATE or both. This should be easy to do with test
> variants.

OK, adding one to the selftests.

> > +	/*
> > +	 * 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)->rights = access_rights;
> > +
> > +	return 0;
> > +}

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 5/5] landlock: Document Landlock's file truncation support
  2022-09-12 19:15   ` Mickaël Salaün
@ 2022-09-23 11:30     ` Günther Noack
  0 siblings, 0 replies; 39+ messages in thread
From: Günther Noack @ 2022-09-23 11:30 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Mon, Sep 12, 2022 at 09:15:06PM +0200, Mickaël Salaün wrote:
> 
> 
> On 08/09/2022 21:58, Günther Noack wrote:
> > Use the LANDLOCK_ACCESS_FS_TRUNCATE flag in the tutorial.
> > 
> > Adapt the backwards compatibility example and discussion to remove the
> > truncation flag where needed.
> > 
> > Point out potential surprising behaviour related to truncate.
> > 
> > Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> > ---
> >   Documentation/userspace-api/landlock.rst | 62 +++++++++++++++++++++---
> >   1 file changed, 54 insertions(+), 8 deletions(-)
> > 
> > diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
> > index b8ea59493964..57802fd1e09b 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: May 2022
> > +:Date: September 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,26 @@ 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) {
> > -        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
> > +    switch (abi) {
> > +    case -1:
> > +            perror("The running kernel does not enable to use Landlock");
> > +            return 1;
> 
> I think it would be easier to understand to explicitly check for abi < 0
> in a dedicated block as in the sample, instead of case -1, and return 0
> (instead of 1) with a comment to inform that Landlock is not handled but
> it is OK (expected error).

Done.

> 
> 
> > +    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.

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 3/5] selftests/landlock: Selftests for file truncation support
  2022-09-16 17:05   ` Mickaël Salaün
@ 2022-09-23 17:50     ` Günther Noack
  2022-09-23 20:54       ` Mickaël Salaün
  0 siblings, 1 reply; 39+ messages in thread
From: Günther Noack @ 2022-09-23 17:50 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Fri, Sep 16, 2022 at 07:05:44PM +0200, Mickaël Salaün wrote:
> I'd like to have tests similar to base_test.c:ruleset_fd_transfer to check
> ftruncate with different kind of file descriptors and not-sandboxed
> processes. That would require some code refactoring to reuse the FD passing
> code.

Done. I factored out the FD sending and receiving into helper function in common.h.

> On 08/09/2022 21:58, Günther Noack wrote:
> > diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
> > index 87b28d14a1aa..ddc8c7e57e86 100644
> > --- a/tools/testing/selftests/landlock/fs_test.c
> > +++ b/tools/testing/selftests/landlock/fs_test.c
> > ...
> > +TEST_F_FORK(layout1, truncate)
> > +{
> > +	const char *const file_rwt = file1_s1d1;
> > +	const char *const file_rw = file2_s1d1;
> > +	const char *const file_rt = file1_s1d2;
> > +	const char *const file_t = file2_s1d2;
> > +	const char *const file_none = file1_s1d3;
> > +	const char *const dir_t = dir_s2d1;
> > +	const char *const file_in_dir_t = file1_s2d1;
> > +	const char *const dir_w = dir_s3d1;
> > +	const char *const file_in_dir_w = file1_s3d1;
> > +	int file_rwt_fd, file_rw_fd;
> 
> These variables are unused now.

Good catch, done.

> > +TEST_F_FORK(layout1, ftruncate)
> 
> Great!
> 
> > +{
> > +	/*
> > +	 * This test opens a new file descriptor at different stages of
> > +	 * Landlock restriction:
> > +	 *
> > +	 * without restriction:                    ftruncate works
> > +	 * something else but truncate restricted: ftruncate works
> > +	 * truncate restricted and permitted:      ftruncate works
> > +	 * truncate restricted and not permitted:  ftruncate fails
> > +	 *
> > +	 * Whether this works or not is expected to depend on the time when the
> > +	 * FD was opened, not to depend on the time when ftruncate() was
> > +	 * called.
> > +	 */
> > +	const char *const path = file1_s1d1;
> > +	int fd0, fd1, fd2, fd3;
> 
> You can rename them fd_layer0, fd_layer1…

Done.

> > +	fd0 = open(path, O_WRONLY);
> > +	EXPECT_EQ(0, test_ftruncate(fd0));
> > +
> > +	landlock_single_path(_metadata, path,
> > +			     LANDLOCK_ACCESS_FS_READ_FILE |
> > +				     LANDLOCK_ACCESS_FS_WRITE_FILE,
> > +			     LANDLOCK_ACCESS_FS_WRITE_FILE);
> 
> I'd prefer to follow the current way to write rule layers: write all struct
> rule at first and then call each enforcement steps. It is a bit more verbose
> but easier to understand errors. The list of test_ftruncate checks are
> straightforward to follow.

Done.


> > +	fd1 = open(path, O_WRONLY);
> > +	EXPECT_EQ(0, test_ftruncate(fd0));
> > +	EXPECT_EQ(0, test_ftruncate(fd1));
> > +
> > +	landlock_single_path(_metadata, path, LANDLOCK_ACCESS_FS_TRUNCATE,
> > +			     LANDLOCK_ACCESS_FS_TRUNCATE);
> > +
> > +	fd2 = open(path, O_WRONLY);
> > +	EXPECT_EQ(0, test_ftruncate(fd0));
> > +	EXPECT_EQ(0, test_ftruncate(fd1));
> > +	EXPECT_EQ(0, test_ftruncate(fd2));
> > +
> > +	landlock_single_path(_metadata, path,
> > +			     LANDLOCK_ACCESS_FS_TRUNCATE |
> > +				     LANDLOCK_ACCESS_FS_WRITE_FILE,
> > +			     LANDLOCK_ACCESS_FS_WRITE_FILE);
> > +
> > +	fd3 = open(path, O_WRONLY);
> > +	EXPECT_EQ(0, test_ftruncate(fd0));
> > +	EXPECT_EQ(0, test_ftruncate(fd1));
> > +	EXPECT_EQ(0, test_ftruncate(fd2));
> > +	EXPECT_EQ(EACCES, test_ftruncate(fd3));
> > +
> > +	ASSERT_EQ(0, close(fd0));
> > +	ASSERT_EQ(0, close(fd1));
> > +	ASSERT_EQ(0, close(fd2));
> > +	ASSERT_EQ(0, close(fd3));
> > +}
> > +
> >   /* clang-format off */
> >   FIXTURE(layout1_bind) {};
> >   /* clang-format on */

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/5] landlock: Support file truncation
  2022-09-23 11:21     ` Günther Noack
@ 2022-09-23 20:53       ` Mickaël Salaün
  2022-09-25 18:09         ` Günther Noack
  0 siblings, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-23 20:53 UTC (permalink / raw)
  To: Günther Noack
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze


On 23/09/2022 13:21, Günther Noack wrote:
> On Mon, Sep 12, 2022 at 09:41:32PM +0200, Mickaël Salaün wrote:
>>
>> On 08/09/2022 21:58, Günther Noack wrote:
>>> Introduce the LANDLOCK_ACCESS_FS_TRUNCATE flag for file truncation.
>>
>> [...]
>>
>>> @@ -761,6 +762,47 @@ static bool collect_domain_accesses(
>>>    	return ret;
>>>    }
>>> +/**
>>> + * get_path_access_rights - Returns the subset of rights in access_request
>>> + * which are permitted for the given path.
>>> + *
>>> + * @domain: The domain that defines the current restrictions.
>>> + * @path: The path to get access rights for.
>>> + * @access_request: The rights we are interested in.
>>> + *
>>> + * Returns: The access mask of the rights that are permitted on the given path,
>>> + * which are also a subset of access_request (to save some calculation time).
>>> + */
>>> +static inline access_mask_t
>>> +get_path_access_rights(const struct landlock_ruleset *const domain,
>>> +		       const struct path *const path,
>>> +		       access_mask_t access_request)
>>> +{
>>> +	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
>>> +	unsigned long access_bit;
>>> +	unsigned long access_req;
>>> +
>>> +	init_layer_masks(domain, access_request, &layer_masks);
>>> +	if (!check_access_path_dual(domain, path, access_request, &layer_masks,
>>> +				    NULL, 0, NULL, NULL)) {
>>> +		/*
>>> +		 * Return immediately for successful accesses and for cases
>>> +		 * where everything is permitted because the path belongs to an
>>> +		 * internal filesystem.
>>> +		 */
>>> +		return access_request;
>>> +	}
>>> +
>>> +	access_req = access_request;
>>> +	for_each_set_bit(access_bit, &access_req, ARRAY_SIZE(layer_masks)) {
>>> +		if (layer_masks[access_bit]) {
>>> +			/* If any layer vetoed the access right, remove it. */
>>> +			access_request &= ~BIT_ULL(access_bit);
>>> +		}
>>> +	}
>>
>> This seems to be redundant with the value returned by init_layer_masks(),
>> which should be passed to check_access_path_dual() to avoid useless path
>> walk.
> 
> True, I'll use the result of init_layer_masks() to feed it back to
> check_access_path_dual() to avoid a bit of computation.
> 
> Like this:
> 
>          effective_access_request =
> 		init_layer_masks(domain, access_request, &layer_masks);
> 	if (!check_access_path_dual(domain, path, effective_access_request,
> 	    &layer_masks, NULL, 0, NULL, NULL)) {
> 		// ...
> 	}

correct

> 
> Overall, the approach here is:
> 
> * Initialize the layer_masks, so that it has a bit set for every
>    access right in access_request and layer where that access right is
>    handled.
> 
> * check_access_path_dual() with only the first few parameters -- this
>    will clear all the bits in layer masks which are actually permitted
>    according to the individual rules.
> 
>    As a special case, this *may* return 0 immediately, in which case we
>    can (a) save a bit of calculation in the loop below and (b) we might
>    be in the case where access is permitted because it's a file from a
>    special file system (even though not all bits are cleared). If
>    check_access_path_dual() returns 0, we return the full requested
>    access_request that we received as input. >
> * In the loop below, if there are any bits left in layer_masks, those
>    are rights which are not permitted for the given path. We remove
>    these from access_request and return the modified access_request.
> 
> 
>> This function is pretty similar to check_access_path(). Can't you change it
>> to use an access_mask_t pointer and get almost the same thing?
> 
> I'm shying away from this approach. Many of the existing different use
> cases are already realized by "doing if checks deep down". I think it
> would make the code more understandable if we managed to model these
> differences between use cases already at the layer of function calls.
> (This is particularly true for check_access_path_dual(), where in
> order to find out how the "single" case works, you need to disentangle
> to a large extent how the much more complicated dual case works.)

I agree that check_access_path_dual() is complex, but I couldn't find a 
better way.


> 
> If you want to unify these two functions, what do you think of the
> approach of just using get_path_access_rights() instead of
> check_access_path()?
> 
> Basically, it would turn
> 
> return check_access_path(dom, path, access_request);
> 
> into
> 
> if (get_path_access_rights(dom, path, access_request) == access_request)
> 	return 0;
> return -EACCES;
> 
> This is slightly more verbose in the places where it's called, but it
> would be more orthogonal, and it would also clarify that -EACCES is
> the only possible error in the "single" path walk case.
> 
> Let me know what you think.

What about adding an additional argument `access_mask_t *const 
access_allowed` to check_access_path_dual() which returns the set of 
accesses (i.e. access_masked_parent1 & access_masked_parent2) that could 
then be stored to landlock_file(file)->allowed_access? If this argument 
is NULL it should just be ignored. What is left from 
get_path_access_rights() could then be merged into hook_file_open().


> 
>>> +	return access_request;
>>> +}
>>> +
>>>    /**
>>>     * current_check_refer_path - Check if a rename or link action is allowed
>>>     *
>>> @@ -1142,6 +1184,11 @@ 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)
>>> @@ -1159,22 +1206,55 @@ static inline access_mask_t get_file_access(const struct file *const file)
>>>    	/* __FMODE_EXEC is indeed part of f_flags, not f_mode. */
>>>    	if (file->f_flags & __FMODE_EXEC)
>>>    		access |= LANDLOCK_ACCESS_FS_EXECUTE;
>>> +
>>>    	return access;
>>>    }
>>>    static int hook_file_open(struct file *const file)
>>>    {
>>> +	access_mask_t access_req, access_rights;
>>
>> "access_request" is used for access_mask_t, and "access_req" for unsigned
>> int. I'd like to stick to this convention.
> 
> Done.
> 
>>> +	const access_mask_t optional_rights = LANDLOCK_ACCESS_FS_TRUNCATE;
>>
>> You use "rights" often and I'm having some trouble to find a rational for
>> that (compared to "access")…
> 
> Done. Didn't realize you already had a different convention here.
> 
> I'm renaming get_path_access_rights() to get_path_access() as well
> then (and I'll rename get_file_access() to
> get_required_file_open_access() - that's more verbose, but it sounded
> too similar to get_path_access(), and it might be better to clarify
> that this is a helper for the file_open hook). Does that sound
> reasonable?

I think it is better, but I'm not convinced this helper is useful.

> 
> 
>>>    	const struct landlock_ruleset *const dom =
>>>    		landlock_get_current_domain();
>>> -	if (!dom)
>>> +	if (!dom) {
>>> +		/* Grant all rights. */
>>> +		landlock_file(file)->rights = LANDLOCK_MASK_ACCESS_FS;
>>>    		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
>>>    	 * evolution.
>>>    	 */
>>> -	return check_access_path(dom, &file->f_path, get_file_access(file));
>>> +	access_req = get_file_access(file);
>>> +	access_rights = get_path_access_rights(dom, &file->f_path,
>>> +					       access_req | optional_rights);
>>> +	if (access_req & ~access_rights)
>>> +		return -EACCES;
>>
>> We should add a test to make sure this (optional_rights) logic is correct
>> (and doesn't change), with a matrix of cases involving a ruleset handling
>> either FS_WRITE, FS_TRUNCATE or both. This should be easy to do with test
>> variants.
> 
> OK, adding one to the selftests.
> 
>>> +	/*
>>> +	 * 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)->rights = access_rights;
>>> +
>>> +	return 0;
>>> +}
> 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 3/5] selftests/landlock: Selftests for file truncation support
  2022-09-23 17:50     ` Günther Noack
@ 2022-09-23 20:54       ` Mickaël Salaün
  2022-09-25 18:10         ` Günther Noack
  0 siblings, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-23 20:54 UTC (permalink / raw)
  To: Günther Noack
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze


On 23/09/2022 19:50, Günther Noack wrote:
> On Fri, Sep 16, 2022 at 07:05:44PM +0200, Mickaël Salaün wrote:
>> I'd like to have tests similar to base_test.c:ruleset_fd_transfer to check
>> ftruncate with different kind of file descriptors and not-sandboxed
>> processes. That would require some code refactoring to reuse the FD passing
>> code.
> 
> Done. I factored out the FD sending and receiving into helper function in common.h.

Please use a dedicated patch for this refactoring.

> 
>> On 08/09/2022 21:58, Günther Noack wrote:
>>> diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
>>> index 87b28d14a1aa..ddc8c7e57e86 100644
>>> --- a/tools/testing/selftests/landlock/fs_test.c
>>> +++ b/tools/testing/selftests/landlock/fs_test.c
>>> ...
>>> +TEST_F_FORK(layout1, truncate)
>>> +{
>>> +	const char *const file_rwt = file1_s1d1;
>>> +	const char *const file_rw = file2_s1d1;
>>> +	const char *const file_rt = file1_s1d2;
>>> +	const char *const file_t = file2_s1d2;
>>> +	const char *const file_none = file1_s1d3;
>>> +	const char *const dir_t = dir_s2d1;
>>> +	const char *const file_in_dir_t = file1_s2d1;
>>> +	const char *const dir_w = dir_s3d1;
>>> +	const char *const file_in_dir_w = file1_s3d1;
>>> +	int file_rwt_fd, file_rw_fd;
>>
>> These variables are unused now.
> 
> Good catch, done.
> 
>>> +TEST_F_FORK(layout1, ftruncate)
>>
>> Great!
>>
>>> +{
>>> +	/*
>>> +	 * This test opens a new file descriptor at different stages of
>>> +	 * Landlock restriction:
>>> +	 *
>>> +	 * without restriction:                    ftruncate works
>>> +	 * something else but truncate restricted: ftruncate works
>>> +	 * truncate restricted and permitted:      ftruncate works
>>> +	 * truncate restricted and not permitted:  ftruncate fails
>>> +	 *
>>> +	 * Whether this works or not is expected to depend on the time when the
>>> +	 * FD was opened, not to depend on the time when ftruncate() was
>>> +	 * called.
>>> +	 */
>>> +	const char *const path = file1_s1d1;
>>> +	int fd0, fd1, fd2, fd3;
>>
>> You can rename them fd_layer0, fd_layer1…
> 
> Done.
> 
>>> +	fd0 = open(path, O_WRONLY);
>>> +	EXPECT_EQ(0, test_ftruncate(fd0));
>>> +
>>> +	landlock_single_path(_metadata, path,
>>> +			     LANDLOCK_ACCESS_FS_READ_FILE |
>>> +				     LANDLOCK_ACCESS_FS_WRITE_FILE,
>>> +			     LANDLOCK_ACCESS_FS_WRITE_FILE);
>>
>> I'd prefer to follow the current way to write rule layers: write all struct
>> rule at first and then call each enforcement steps. It is a bit more verbose
>> but easier to understand errors. The list of test_ftruncate checks are
>> straightforward to follow.
> 
> Done.
> 
> 
>>> +	fd1 = open(path, O_WRONLY);
>>> +	EXPECT_EQ(0, test_ftruncate(fd0));
>>> +	EXPECT_EQ(0, test_ftruncate(fd1));
>>> +
>>> +	landlock_single_path(_metadata, path, LANDLOCK_ACCESS_FS_TRUNCATE,
>>> +			     LANDLOCK_ACCESS_FS_TRUNCATE);
>>> +
>>> +	fd2 = open(path, O_WRONLY);
>>> +	EXPECT_EQ(0, test_ftruncate(fd0));
>>> +	EXPECT_EQ(0, test_ftruncate(fd1));
>>> +	EXPECT_EQ(0, test_ftruncate(fd2));
>>> +
>>> +	landlock_single_path(_metadata, path,
>>> +			     LANDLOCK_ACCESS_FS_TRUNCATE |
>>> +				     LANDLOCK_ACCESS_FS_WRITE_FILE,
>>> +			     LANDLOCK_ACCESS_FS_WRITE_FILE);
>>> +
>>> +	fd3 = open(path, O_WRONLY);
>>> +	EXPECT_EQ(0, test_ftruncate(fd0));
>>> +	EXPECT_EQ(0, test_ftruncate(fd1));
>>> +	EXPECT_EQ(0, test_ftruncate(fd2));
>>> +	EXPECT_EQ(EACCES, test_ftruncate(fd3));
>>> +
>>> +	ASSERT_EQ(0, close(fd0));
>>> +	ASSERT_EQ(0, close(fd1));
>>> +	ASSERT_EQ(0, close(fd2));
>>> +	ASSERT_EQ(0, close(fd3));
>>> +}
>>> +
>>>    /* clang-format off */
>>>    FIXTURE(layout1_bind) {};
>>>    /* clang-format on */
> 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/5] landlock: Support file truncation
  2022-09-23 20:53       ` Mickaël Salaün
@ 2022-09-25 18:09         ` Günther Noack
  2022-09-28 18:32           ` Mickaël Salaün
  0 siblings, 1 reply; 39+ messages in thread
From: Günther Noack @ 2022-09-25 18:09 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Fri, Sep 23, 2022 at 10:53:23PM +0200, Mickaël Salaün wrote:
> On 23/09/2022 13:21, Günther Noack wrote:
> > On Mon, Sep 12, 2022 at 09:41:32PM +0200, Mickaël Salaün wrote:
> > > On 08/09/2022 21:58, Günther Noack wrote:
> > > > Introduce the LANDLOCK_ACCESS_FS_TRUNCATE flag for file truncation.
> > > 
> > > [...]
> > > 
> > > > +/**
> > > > + * get_path_access_rights - Returns the subset of rights in access_request
> > > > + * which are permitted for the given path.
> > > > + *
> > > > + * @domain: The domain that defines the current restrictions.
> > > > + * @path: The path to get access rights for.
> > > > + * @access_request: The rights we are interested in.
> > > > + *
> > > > + * Returns: The access mask of the rights that are permitted on the given path,
> > > > + * which are also a subset of access_request (to save some calculation time).
> > > > + */
> > > > +static inline access_mask_t
> > > > +get_path_access_rights(const struct landlock_ruleset *const domain,
> > > > +		       const struct path *const path,
> > > > +		       access_mask_t access_request)
> > > > +{
> > > > +	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
> > > > +	unsigned long access_bit;
> > > > +	unsigned long access_req;
> > > > +
> > > > +	init_layer_masks(domain, access_request, &layer_masks);
> > > > +	if (!check_access_path_dual(domain, path, access_request, &layer_masks,
> > > > +				    NULL, 0, NULL, NULL)) {
> > > > +		/*
> > > > +		 * Return immediately for successful accesses and for cases
> > > > +		 * where everything is permitted because the path belongs to an
> > > > +		 * internal filesystem.
> > > > +		 */
> > > > +		return access_request;
> > > > +	}
> > > > +
> > > > +	access_req = access_request;
> > > > +	for_each_set_bit(access_bit, &access_req, ARRAY_SIZE(layer_masks)) {
> > > > +		if (layer_masks[access_bit]) {
> > > > +			/* If any layer vetoed the access right, remove it. */
> > > > +			access_request &= ~BIT_ULL(access_bit);
> > > > +		}
> > > > +	}
> > > 
> > > This seems to be redundant with the value returned by init_layer_masks(),
> > > which should be passed to check_access_path_dual() to avoid useless path
> > > walk.
> > 
> > True, I'll use the result of init_layer_masks() to feed it back to
> > check_access_path_dual() to avoid a bit of computation.
> > 
> > Like this:
> > 
> >          effective_access_request =
> > 		init_layer_masks(domain, access_request, &layer_masks);
> > 	if (!check_access_path_dual(domain, path, effective_access_request,
> > 	    &layer_masks, NULL, 0, NULL, NULL)) {
> > 		// ...
> > 	}
> 
> correct
> 
> > 
> > Overall, the approach here is:
> > 
> > * Initialize the layer_masks, so that it has a bit set for every
> >    access right in access_request and layer where that access right is
> >    handled.
> > 
> > * check_access_path_dual() with only the first few parameters -- this
> >    will clear all the bits in layer masks which are actually permitted
> >    according to the individual rules.
> > 
> >    As a special case, this *may* return 0 immediately, in which case we
> >    can (a) save a bit of calculation in the loop below and (b) we might
> >    be in the case where access is permitted because it's a file from a
> >    special file system (even though not all bits are cleared). If
> >    check_access_path_dual() returns 0, we return the full requested
> >    access_request that we received as input. >
> > * In the loop below, if there are any bits left in layer_masks, those
> >    are rights which are not permitted for the given path. We remove
> >    these from access_request and return the modified access_request.
> > 
> > 
> > > This function is pretty similar to check_access_path(). Can't you change it
> > > to use an access_mask_t pointer and get almost the same thing?
> > 
> > I'm shying away from this approach. Many of the existing different use
> > cases are already realized by "doing if checks deep down". I think it
> > would make the code more understandable if we managed to model these
> > differences between use cases already at the layer of function calls.
> > (This is particularly true for check_access_path_dual(), where in
> > order to find out how the "single" case works, you need to disentangle
> > to a large extent how the much more complicated dual case works.)
> 
> I agree that check_access_path_dual() is complex, but I couldn't find a
> better way.

It seems out of the scope of this patch set, but I sometimes find it
OK to just duplicate the code and have a set of tests to demonstrate
that the two variants do the same thing.

check_access_path_dual() is mostly complex because of performance
reasons, as far as I can tell, and it might be possible to check its
results against a parallel implementation of it which runs slower,
uses more memory, but is more obviously correct. (I have used one
myself to check against when developing the truncate patch set.)

> > If you want to unify these two functions, what do you think of the
> > approach of just using get_path_access_rights() instead of
> > check_access_path()?
> > 
> > Basically, it would turn
> > 
> > return check_access_path(dom, path, access_request);
> > 
> > into
> > 
> > if (get_path_access_rights(dom, path, access_request) == access_request)
> > 	return 0;
> > return -EACCES;
> > 
> > This is slightly more verbose in the places where it's called, but it
> > would be more orthogonal, and it would also clarify that -EACCES is
> > the only possible error in the "single" path walk case.
> > 
> > Let me know what you think.
> 
> What about adding an additional argument `access_mask_t *const
> access_allowed` to check_access_path_dual() which returns the set of
> accesses (i.e. access_masked_parent1 & access_masked_parent2) that could
> then be stored to landlock_file(file)->allowed_access? If this argument is
> NULL it should just be ignored. What is left from get_path_access_rights()
> could then be merged into hook_file_open().

IMHO, check_access_path_dual() does not seem like the right place to
add this. This functionality is not needed in any of the "dual path"
cases so far, and I'm not sure what it would mean. The necessary
information can also be easily derived from the resulting layer_masks,
which is already exposed in the check_access_path_dual() interface,
and I also believe that this approach is at least equally fast as
updating it on the fly when changing the layer_masks.

I could be convinced to add a `access_mask_t *const access_allowed`
argument to check_access_path() if you prefer that, but then again, in
that case the returned boolean can be reconstructed from the new
access_allowed variable, and we could as well make check_access_path()
return the access_allowed result instead of the boolean and let
callers check equality with what they expected...? (I admittedly don't
have a good setup to test the performance right now, but it looks like
a negligible difference to me?)

Here are the options we have discussed, in the order that I would
prefer them:

* to keep it as a separate function as it already is,
  slightly duplicating check_access_path(). (I think it's cleaner,
  because the code path for the rest of the hooks other than
  security_file_open() stays simpler.)

* to make check_access_path() return the access_allowed access mask
  and make callers check that it covers the access_request that they
  asked for (see example from my previous mail on this thread). (This
  is equivalent to discarding the existing check_access_path() and
  using the get_path_access() function instead.)

* to add a `access_mask_t *const access_allowed` argument to
  check_access_path(), which is calculated if it's non-NULL based on
  the layer_masks result. It would be used from the security_file_open
  hook.

* to add a `access_mask_t *const access_allowed` argument to
  check_access_path_dual(). This doesn't make much sense, IMHO,
  because an on-the-fly calculation of this result does not look like
  a performance benefit to me, and calculating it based on the two
  resulting layer_masks is already possible now. It's also not clear
  to me what it would mean to calculate an access_allowed on two paths
  at once, and what that would be used for.

Let me know which option you prefer. In the end, I don't feel that
strongly about it and I'm happy to do this either way.


> > > > +	return access_request;
> > > > +}
> > > > +
> > > >    /**
> > > >     * current_check_refer_path - Check if a rename or link action is allowed
> > > >     *
> > > > @@ -1142,6 +1184,11 @@ 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)
> > > > @@ -1159,22 +1206,55 @@ static inline access_mask_t get_file_access(const struct file *const file)
> > > >    	/* __FMODE_EXEC is indeed part of f_flags, not f_mode. */
> > > >    	if (file->f_flags & __FMODE_EXEC)
> > > >    		access |= LANDLOCK_ACCESS_FS_EXECUTE;
> > > > +
> > > >    	return access;
> > > >    }
> > > >    static int hook_file_open(struct file *const file)
> > > >    {
> > > > +	access_mask_t access_req, access_rights;
> > > 
> > > "access_request" is used for access_mask_t, and "access_req" for unsigned
> > > int. I'd like to stick to this convention.
> > 
> > Done.
> > 
> > > > +	const access_mask_t optional_rights = LANDLOCK_ACCESS_FS_TRUNCATE;
> > > 
> > > You use "rights" often and I'm having some trouble to find a rational for
> > > that (compared to "access")…
> > 
> > Done. Didn't realize you already had a different convention here.
> > 
> > I'm renaming get_path_access_rights() to get_path_access() as well
> > then (and I'll rename get_file_access() to
> > get_required_file_open_access() - that's more verbose, but it sounded
> > too similar to get_path_access(), and it might be better to clarify
> > that this is a helper for the file_open hook). Does that sound
> > reasonable?
> 
> I think it is better, but I'm not convinced this helper is useful.
> 
> > 
> > 
> > > >    	const struct landlock_ruleset *const dom =
> > > >    		landlock_get_current_domain();
> > > > -	if (!dom)
> > > > +	if (!dom) {
> > > > +		/* Grant all rights. */
> > > > +		landlock_file(file)->rights = LANDLOCK_MASK_ACCESS_FS;
> > > >    		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
> > > >    	 * evolution.
> > > >    	 */
> > > > -	return check_access_path(dom, &file->f_path, get_file_access(file));
> > > > +	access_req = get_file_access(file);
> > > > +	access_rights = get_path_access_rights(dom, &file->f_path,
> > > > +					       access_req | optional_rights);
> > > > +	if (access_req & ~access_rights)
> > > > +		return -EACCES;
> > > 
> > > We should add a test to make sure this (optional_rights) logic is correct
> > > (and doesn't change), with a matrix of cases involving a ruleset handling
> > > either FS_WRITE, FS_TRUNCATE or both. This should be easy to do with test
> > > variants.
> > 
> > OK, adding one to the selftests.
> > 
> > > > +	/*
> > > > +	 * 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)->rights = access_rights;
> > > > +
> > > > +	return 0;
> > > > +}
> > 

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 3/5] selftests/landlock: Selftests for file truncation support
  2022-09-23 20:54       ` Mickaël Salaün
@ 2022-09-25 18:10         ` Günther Noack
  0 siblings, 0 replies; 39+ messages in thread
From: Günther Noack @ 2022-09-25 18:10 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Fri, Sep 23, 2022 at 10:54:55PM +0200, Mickaël Salaün wrote:
> 
> On 23/09/2022 19:50, Günther Noack wrote:
> > On Fri, Sep 16, 2022 at 07:05:44PM +0200, Mickaël Salaün wrote:
> > > I'd like to have tests similar to base_test.c:ruleset_fd_transfer to check
> > > ftruncate with different kind of file descriptors and not-sandboxed
> > > processes. That would require some code refactoring to reuse the FD passing
> > > code.
> > 
> > Done. I factored out the FD sending and receiving into helper function in common.h.
> 
> Please use a dedicated patch for this refactoring.

+1, will do.

> > > On 08/09/2022 21:58, Günther Noack wrote:
> > > > diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
> > > > index 87b28d14a1aa..ddc8c7e57e86 100644
> > > > --- a/tools/testing/selftests/landlock/fs_test.c
> > > > +++ b/tools/testing/selftests/landlock/fs_test.c
> > > > ...
> > > > +TEST_F_FORK(layout1, truncate)
> > > > +{
> > > > +	const char *const file_rwt = file1_s1d1;
> > > > +	const char *const file_rw = file2_s1d1;
> > > > +	const char *const file_rt = file1_s1d2;
> > > > +	const char *const file_t = file2_s1d2;
> > > > +	const char *const file_none = file1_s1d3;
> > > > +	const char *const dir_t = dir_s2d1;
> > > > +	const char *const file_in_dir_t = file1_s2d1;
> > > > +	const char *const dir_w = dir_s3d1;
> > > > +	const char *const file_in_dir_w = file1_s3d1;
> > > > +	int file_rwt_fd, file_rw_fd;
> > > 
> > > These variables are unused now.
> > 
> > Good catch, done.
> > 
> > > > +TEST_F_FORK(layout1, ftruncate)
> > > 
> > > Great!
> > > 
> > > > +{
> > > > +	/*
> > > > +	 * This test opens a new file descriptor at different stages of
> > > > +	 * Landlock restriction:
> > > > +	 *
> > > > +	 * without restriction:                    ftruncate works
> > > > +	 * something else but truncate restricted: ftruncate works
> > > > +	 * truncate restricted and permitted:      ftruncate works
> > > > +	 * truncate restricted and not permitted:  ftruncate fails
> > > > +	 *
> > > > +	 * Whether this works or not is expected to depend on the time when the
> > > > +	 * FD was opened, not to depend on the time when ftruncate() was
> > > > +	 * called.
> > > > +	 */
> > > > +	const char *const path = file1_s1d1;
> > > > +	int fd0, fd1, fd2, fd3;
> > > 
> > > You can rename them fd_layer0, fd_layer1…
> > 
> > Done.
> > 
> > > > +	fd0 = open(path, O_WRONLY);
> > > > +	EXPECT_EQ(0, test_ftruncate(fd0));
> > > > +
> > > > +	landlock_single_path(_metadata, path,
> > > > +			     LANDLOCK_ACCESS_FS_READ_FILE |
> > > > +				     LANDLOCK_ACCESS_FS_WRITE_FILE,
> > > > +			     LANDLOCK_ACCESS_FS_WRITE_FILE);
> > > 
> > > I'd prefer to follow the current way to write rule layers: write all struct
> > > rule at first and then call each enforcement steps. It is a bit more verbose
> > > but easier to understand errors. The list of test_ftruncate checks are
> > > straightforward to follow.
> > 
> > Done.
> > 
> > 
> > > > +	fd1 = open(path, O_WRONLY);
> > > > +	EXPECT_EQ(0, test_ftruncate(fd0));
> > > > +	EXPECT_EQ(0, test_ftruncate(fd1));
> > > > +
> > > > +	landlock_single_path(_metadata, path, LANDLOCK_ACCESS_FS_TRUNCATE,
> > > > +			     LANDLOCK_ACCESS_FS_TRUNCATE);
> > > > +
> > > > +	fd2 = open(path, O_WRONLY);
> > > > +	EXPECT_EQ(0, test_ftruncate(fd0));
> > > > +	EXPECT_EQ(0, test_ftruncate(fd1));
> > > > +	EXPECT_EQ(0, test_ftruncate(fd2));
> > > > +
> > > > +	landlock_single_path(_metadata, path,
> > > > +			     LANDLOCK_ACCESS_FS_TRUNCATE |
> > > > +				     LANDLOCK_ACCESS_FS_WRITE_FILE,
> > > > +			     LANDLOCK_ACCESS_FS_WRITE_FILE);
> > > > +
> > > > +	fd3 = open(path, O_WRONLY);
> > > > +	EXPECT_EQ(0, test_ftruncate(fd0));
> > > > +	EXPECT_EQ(0, test_ftruncate(fd1));
> > > > +	EXPECT_EQ(0, test_ftruncate(fd2));
> > > > +	EXPECT_EQ(EACCES, test_ftruncate(fd3));
> > > > +
> > > > +	ASSERT_EQ(0, close(fd0));
> > > > +	ASSERT_EQ(0, close(fd1));
> > > > +	ASSERT_EQ(0, close(fd2));
> > > > +	ASSERT_EQ(0, close(fd3));
> > > > +}
> > > > +
> > > >    /* clang-format off */
> > > >    FIXTURE(layout1_bind) {};
> > > >    /* clang-format on */
> > 

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook
  2022-09-16 17:30     ` Mickaël Salaün
@ 2022-09-26 16:07       ` Günther Noack
  2022-09-28 20:04         ` Mickaël Salaün
  0 siblings, 1 reply; 39+ messages in thread
From: Günther Noack @ 2022-09-26 16:07 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Namjae Jeon, David Howells, Christian Brauner, James Morris,
	Paul Moore, Serge E . Hallyn, linux-fsdevel,
	Konstantin Meskhidze, linux-security-module

On Fri, Sep 16, 2022 at 07:30:24PM +0200, Mickaël Salaün wrote:
> We may indeed need to change fs/open.c:vfs_truncate() because of these
> different call sites. I'm not sure how these subsystems work though.

I thought about this some more, and I'm coming around to the
conclusion that we should not block the truncate patch set on changes
in ksmbd and cachefiles.

The reasoning is:

* Landlock does already work for ksmbd and cachefiles. vfs_truncate
  does call the security_path_truncate() hook in the background.

* ksmbd and cachefiles using vfs_truncate() in kernel space is roughly
  equivalent to a user space program using truncate(2) in a place
  where ftruncate(2) is possible. It might not be the most elegant
  approach, but it's legitimate to do.

* Like with any userspace program that is supposed to run under
  Landlock, ksmbd and cachefiles both may need to be adapted slightly
  to work well with Landlock enforcement. It is up to the person
  adding the Landlock enforcement to double check that the program
  works correctly under the enforced ruleset. This is true for both
  programs running in user space and kernel space.

So yes, to run ksmbd and cachefiles under Landlock, we may need to
extract a fs/open.c:vfs_ftruncate() in addition to vfs_truncate(), but
I don't think it should be part of this patch set.

So my proposal would be to:

* not do the ksmbd and cachefiles changes now,

* but leave them for later when someone actually tries to run ksmbd or
  cachefiles under Landlock.

If these components never get executed in a Landlocked context, all
the better - we can spare ourselves a more complicated refactoring in
a core part of the kernel.

FWIW, I've played around with it yesterday and found that the change
to extract a new "vfs_ftruncate()" next to vfs_truncate() is
reasonably self-contained. But I'm not a file system expert either,
it's well possible that I'm overlooking something.

Let me know what you think!

> On 08/09/2022 22:28, Günther Noack wrote:
> > Adding Namjae Jeon and David Howells as authors of the respective
> > files in fs/ksmbd and fs/cachefiles -- do you happen to know whether
> > these vfs_truncate() calls are using 'struct file's that are opened by
> > normal userspace processes, where LSM policies may apply?
> > 
> > P.S. In this patch I have looked for all places where the
> > security_path_truncate() hook was called, to see which of these should
> > rather use security_file_truncate() (and I made sure that it does the
> > same thing for all the LSMs that use it).
> > 
> > I'm confident that this does the right thing when truncate() or
> > ftruncate() are called from userspace, but one of the places that
> > still calls the path-based hook is vfs_truncate(), and this is called
> > from more places in the kernel than just from userspace:
> > 
> > init/initramfs.c
> > 387:				vfs_truncate(&wfile->f_path, body_len);
> > 
> > security/keys/big_key.c
> > 172:		vfs_truncate(&payload->path, 0);
> > 
> > fs/cachefiles/interface.c
> > 242:		ret = vfs_truncate(&file->f_path, dio_size);
> > 
> > fs/cachefiles/namei.c
> > 497:			ret = vfs_truncate(&path, ni_size); >
> > fs/ksmbd/smb2pdu.c
> > 2350:	int rc = vfs_truncate(path, 0);
> > 
> > fs/ksmbd/vfs.c
> > 874:	err = vfs_truncate(&filp->f_path, size);
> > 
> > I suspect that these are benign but am not familiar with all of these
> > corners of the codebase. -- The question is: Some of these call
> > vfs_truncate() on the f_path of an existing struct file -- should
> > these rather be calling the security_file_truncate() than the
> > security_path_truncate() hook to authorize the truncation?
> > 
> > Specifically, I think:
> > 
> > * initramfs happens at system startup and LSMs should not interfere at
> >    this point yet
> > * security/keys does not use an opened struct file, so calling the
> >    path-based hook through vfs_truncate() is correct
> > * fs/cachefiles and fs/ksmbd use the file system from the kernel to
> >    expose it as another file system (in a cached form for cachefiles,
> >    and over the network for ksmbd). I suspect that these file systems
> >    are not handling 'struct file's which are opened in contexts where a
> >    LSM applies? It that a reasonable assumption?
> 
> I think you're right but I have some doubts about the cachefiles subsystem.
> I don't know how ksmb deals with these file descriptors but changing such
> call sites (where there is a struct file) could improve API consistency
> though.
> Any though?

My conclusion is already summarized above, and I've tried to abstract
away from the concrete use cases. For completeness, I've also looked
into ksmbd and cachefiles specifically though so see whether
security_path_truncate and security_file_truncate would make a
difference.

For ksmbd, I strongly suspect it does not make a difference (90%
confidence) -- the files are getting opened by the same request
handler context which is also truncating the files later on behalf of
a truncation operation in the SMB protocol. It's anyway unclear to me
whether the kernel tasks executing this can be put under Landlock
enforcement at all..?

fs/cachefiles is a more layered system and uses some
cachefiles-independent caching structures with void* pointers, whose
values I found difficult to trace. I'm less certain about this one as
well, but as discussed above, it does not make a difference as long as
none of the cachefiles code executes in a Landlock context. I'm still
in favor of decoupling potential ksmbd and cachefiles changes from
this patch set.

—Günther

> 
> 
> > 
> > Thanks,
> > Günther
> > 
> > On Thu, Sep 08, 2022 at 09:58:01PM +0200, Günther Noack wrote:
> > > Like path_truncate, the file_truncate hook also restricts file
> > > truncation, but is called in the cases where truncation is attempted
> > > on an already-opened file.
> > > 
> > > This is required in a subsequent commit to handle ftruncate()
> > > operations differently to truncate() operations.
> > > 
> > > Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> > > ---
> > >   fs/namei.c                    |  6 +++---
> > >   fs/open.c                     |  4 ++--
> > >   include/linux/lsm_hook_defs.h |  1 +
> > >   include/linux/security.h      |  6 ++++++
> > >   security/apparmor/lsm.c       |  6 ++++++
> > >   security/security.c           |  5 +++++
> > >   security/tomoyo/tomoyo.c      | 13 +++++++++++++
> > >   7 files changed, 36 insertions(+), 5 deletions(-)
> > > 
> > > diff --git a/fs/namei.c b/fs/namei.c
> > > index 53b4bc094db2..52105873d1f8 100644
> > > --- a/fs/namei.c
> > > +++ b/fs/namei.c
> > > @@ -53,8 +53,8 @@
> > >    * The new code replaces the old recursive symlink resolution with
> > >    * an iterative one (in case of non-nested symlink chains).  It does
> > >    * this with calls to <fs>_follow_link().
> > > - * As a side effect, dir_namei(), _namei() and follow_link() are now
> > > - * replaced with a single function lookup_dentry() that can handle all
> > > + * As a side effect, dir_namei(), _namei() and follow_link() are now
> > > + * replaced with a single function lookup_dentry() that can handle all
> > >    * the special cases of the former code.
> > >    *
> > >    * With the new dcache, the pathname is stored at each inode, at least as
> > > @@ -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 8a813fa5ca56..0831433e493a 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);
> > > @@ -1271,7 +1271,7 @@ struct file *filp_open(const char *filename, int flags, umode_t mode)
> > >   {
> > >   	struct filename *name = getname_kernel(filename);
> > >   	struct file *file = ERR_CAST(name);
> > > -
> > > +
> > >   	if (!IS_ERR(name)) {
> > >   		file = file_open_name(name, flags, mode);
> > >   		putname(name);
> > > diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
> > > index 60fff133c0b1..dee35ab253ba 100644
> > > --- a/include/linux/lsm_hook_defs.h
> > > +++ b/include/linux/lsm_hook_defs.h
> > > @@ -177,6 +177,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/security.h b/include/linux/security.h
> > > index 7bd0c490703d..f80b23382dd9 100644
> > > --- a/include/linux/security.h
> > > +++ b/include/linux/security.h
> > > @@ -394,6 +394,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);
> > > @@ -1011,6 +1012,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/security/apparmor/lsm.c b/security/apparmor/lsm.c
> > > index e29cade7b662..98ecb7f221b8 100644
> > > --- a/security/apparmor/lsm.c
> > > +++ b/security/apparmor/lsm.c
> > > @@ -329,6 +329,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)
> > >   {
> > > @@ -1232,6 +1237,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/security.c b/security/security.c
> > > index 4b95de24bc8d..e491120c48ba 100644
> > > --- a/security/security.c
> > > +++ b/security/security.c
> > > @@ -1210,6 +1210,11 @@ int security_path_truncate(const struct path *path)
> > >   	return call_int_hook(path_truncate, 0, path);
> > >   }
> > > 
> > > +int security_file_truncate(struct file *file)
> > > +{
> > > +	return call_int_hook(file_truncate, 0, file);
> > > +}
> > > +
> > >   int security_path_chmod(const struct path *path, umode_t mode)
> > >   {
> > >   	if (unlikely(IS_PRIVATE(d_backing_inode(path->dentry))))
> > > 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
> > > @@ -134,6 +134,18 @@ static int tomoyo_path_truncate(const struct path *path)
> > >   	return tomoyo_path_perm(TOMOYO_TYPE_TRUNCATE, path, NULL);
> > >   }
> > > 
> > > +/**
> > > + * 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().
> > >    *
> > > @@ -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),
> > > --
> > > 2.37.3
> > > 
> > 
> > --

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/5] landlock: Support file truncation
  2022-09-25 18:09         ` Günther Noack
@ 2022-09-28 18:32           ` Mickaël Salaün
  2022-09-29 19:22             ` Günther Noack
  0 siblings, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-28 18:32 UTC (permalink / raw)
  To: Günther Noack
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze


On 25/09/2022 20:09, Günther Noack wrote:
> On Fri, Sep 23, 2022 at 10:53:23PM +0200, Mickaël Salaün wrote:
>> On 23/09/2022 13:21, Günther Noack wrote:
>>> On Mon, Sep 12, 2022 at 09:41:32PM +0200, Mickaël Salaün wrote:
>>>> On 08/09/2022 21:58, Günther Noack wrote:
>>>>> Introduce the LANDLOCK_ACCESS_FS_TRUNCATE flag for file truncation.
>>>>
>>>> [...]
>>>>
>>>>> +/**
>>>>> + * get_path_access_rights - Returns the subset of rights in access_request
>>>>> + * which are permitted for the given path.
>>>>> + *
>>>>> + * @domain: The domain that defines the current restrictions.
>>>>> + * @path: The path to get access rights for.
>>>>> + * @access_request: The rights we are interested in.
>>>>> + *
>>>>> + * Returns: The access mask of the rights that are permitted on the given path,
>>>>> + * which are also a subset of access_request (to save some calculation time).
>>>>> + */
>>>>> +static inline access_mask_t
>>>>> +get_path_access_rights(const struct landlock_ruleset *const domain,
>>>>> +		       const struct path *const path,
>>>>> +		       access_mask_t access_request)
>>>>> +{
>>>>> +	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
>>>>> +	unsigned long access_bit;
>>>>> +	unsigned long access_req;
>>>>> +
>>>>> +	init_layer_masks(domain, access_request, &layer_masks);
>>>>> +	if (!check_access_path_dual(domain, path, access_request, &layer_masks,
>>>>> +				    NULL, 0, NULL, NULL)) {
>>>>> +		/*
>>>>> +		 * Return immediately for successful accesses and for cases
>>>>> +		 * where everything is permitted because the path belongs to an
>>>>> +		 * internal filesystem.
>>>>> +		 */
>>>>> +		return access_request;
>>>>> +	}
>>>>> +
>>>>> +	access_req = access_request;
>>>>> +	for_each_set_bit(access_bit, &access_req, ARRAY_SIZE(layer_masks)) {
>>>>> +		if (layer_masks[access_bit]) {
>>>>> +			/* If any layer vetoed the access right, remove it. */
>>>>> +			access_request &= ~BIT_ULL(access_bit);
>>>>> +		}
>>>>> +	}
>>>>
>>>> This seems to be redundant with the value returned by init_layer_masks(),
>>>> which should be passed to check_access_path_dual() to avoid useless path
>>>> walk.
>>>
>>> True, I'll use the result of init_layer_masks() to feed it back to
>>> check_access_path_dual() to avoid a bit of computation.
>>>
>>> Like this:
>>>
>>>           effective_access_request =
>>> 		init_layer_masks(domain, access_request, &layer_masks);
>>> 	if (!check_access_path_dual(domain, path, effective_access_request,
>>> 	    &layer_masks, NULL, 0, NULL, NULL)) {
>>> 		// ...
>>> 	}
>>
>> correct
>>
>>>
>>> Overall, the approach here is:
>>>
>>> * Initialize the layer_masks, so that it has a bit set for every
>>>     access right in access_request and layer where that access right is
>>>     handled.
>>>
>>> * check_access_path_dual() with only the first few parameters -- this
>>>     will clear all the bits in layer masks which are actually permitted
>>>     according to the individual rules.
>>>
>>>     As a special case, this *may* return 0 immediately, in which case we
>>>     can (a) save a bit of calculation in the loop below and (b) we might
>>>     be in the case where access is permitted because it's a file from a
>>>     special file system (even though not all bits are cleared). If
>>>     check_access_path_dual() returns 0, we return the full requested
>>>     access_request that we received as input. >
>>> * In the loop below, if there are any bits left in layer_masks, those
>>>     are rights which are not permitted for the given path. We remove
>>>     these from access_request and return the modified access_request.
>>>
>>>
>>>> This function is pretty similar to check_access_path(). Can't you change it
>>>> to use an access_mask_t pointer and get almost the same thing?
>>>
>>> I'm shying away from this approach. Many of the existing different use
>>> cases are already realized by "doing if checks deep down". I think it
>>> would make the code more understandable if we managed to model these
>>> differences between use cases already at the layer of function calls.
>>> (This is particularly true for check_access_path_dual(), where in
>>> order to find out how the "single" case works, you need to disentangle
>>> to a large extent how the much more complicated dual case works.)
>>
>> I agree that check_access_path_dual() is complex, but I couldn't find a
>> better way.
> 
> It seems out of the scope of this patch set, but I sometimes find it
> OK to just duplicate the code and have a set of tests to demonstrate
> that the two variants do the same thing.
> 
> check_access_path_dual() is mostly complex because of performance
> reasons, as far as I can tell, and it might be possible to check its
> results against a parallel implementation of it which runs slower,
> uses more memory, but is more obviously correct. (I have used one
> myself to check against when developing the truncate patch set.)
> 
>>> If you want to unify these two functions, what do you think of the
>>> approach of just using get_path_access_rights() instead of
>>> check_access_path()?
>>>
>>> Basically, it would turn
>>>
>>> return check_access_path(dom, path, access_request);
>>>
>>> into
>>>
>>> if (get_path_access_rights(dom, path, access_request) == access_request)
>>> 	return 0;
>>> return -EACCES;
>>>
>>> This is slightly more verbose in the places where it's called, but it
>>> would be more orthogonal, and it would also clarify that -EACCES is
>>> the only possible error in the "single" path walk case.
>>>
>>> Let me know what you think.
>>
>> What about adding an additional argument `access_mask_t *const
>> access_allowed` to check_access_path_dual() which returns the set of
>> accesses (i.e. access_masked_parent1 & access_masked_parent2) that could
>> then be stored to landlock_file(file)->allowed_access? If this argument is
>> NULL it should just be ignored. What is left from get_path_access_rights()
>> could then be merged into hook_file_open().
> 
> IMHO, check_access_path_dual() does not seem like the right place to
> add this. This functionality is not needed in any of the "dual path"
> cases so far, and I'm not sure what it would mean. The necessary
> information can also be easily derived from the resulting layer_masks,
> which is already exposed in the check_access_path_dual() interface,
> and I also believe that this approach is at least equally fast as
> updating it on the fly when changing the layer_masks.
> 
> I could be convinced to add a `access_mask_t *const access_allowed`
> argument to check_access_path() if you prefer that, but then again, in
> that case the returned boolean can be reconstructed from the new
> access_allowed variable, and we could as well make check_access_path()
> return the access_allowed result instead of the boolean and let
> callers check equality with what they expected...? (I admittedly don't
> have a good setup to test the performance right now, but it looks like
> a negligible difference to me?)

Good idea, let's try to make check_access_path_dual() returns the 
allowed accesses (according to the request) and rename it to 
get_access_path_dual(). unmask_layers() could be changed to return the 
still-denied accesses instead of a boolean, and we could use this values 
(for potential both parents) to return allowed_parent1 & allowed_parent2 
(with access_mask_t types). This would also simplify is_eaccess() and 
its calls could be moved to current_check_refer_path(). This would merge 
get_path_access_rights() into check_access_path_dual() and make the 
errno codes more explicit per hook or defined in check_access_path().


> 
> Here are the options we have discussed, in the order that I would
> prefer them:
> 
> * to keep it as a separate function as it already is,
>    slightly duplicating check_access_path(). (I think it's cleaner,
>    because the code path for the rest of the hooks other than
>    security_file_open() stays simpler.)
> 
> * to make check_access_path() return the access_allowed access mask
>    and make callers check that it covers the access_request that they
>    asked for (see example from my previous mail on this thread). (This
>    is equivalent to discarding the existing check_access_path() and
>    using the get_path_access() function instead.)
> 
> * to add a `access_mask_t *const access_allowed` argument to
>    check_access_path(), which is calculated if it's non-NULL based on
>    the layer_masks result. It would be used from the security_file_open
>    hook.
> 
> * to add a `access_mask_t *const access_allowed` argument to
>    check_access_path_dual(). This doesn't make much sense, IMHO,
>    because an on-the-fly calculation of this result does not look like
>    a performance benefit to me, and calculating it based on the two
>    resulting layer_masks is already possible now. It's also not clear
>    to me what it would mean to calculate an access_allowed on two paths
>    at once, and what that would be used for.
> 
> Let me know which option you prefer. In the end, I don't feel that
> strongly about it and I'm happy to do this either way.

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook
  2022-09-26 16:07       ` Günther Noack
@ 2022-09-28 20:04         ` Mickaël Salaün
  2022-09-29  2:55           ` Namjae Jeon
  0 siblings, 1 reply; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-28 20:04 UTC (permalink / raw)
  To: Günther Noack, David Howells, Christian Brauner
  Cc: Namjae Jeon, Steve French, Hyunchul Lee, James Morris,
	Paul Moore, Serge E . Hallyn, linux-fsdevel,
	Konstantin Meskhidze, linux-security-module, linux-cifs, Al Viro


On 26/09/2022 18:07, Günther Noack wrote:
> On Fri, Sep 16, 2022 at 07:30:24PM +0200, Mickaël Salaün wrote:
>> We may indeed need to change fs/open.c:vfs_truncate() because of these
>> different call sites. I'm not sure how these subsystems work though.
> 
> I thought about this some more, and I'm coming around to the
> conclusion that we should not block the truncate patch set on changes
> in ksmbd and cachefiles.
> 
> The reasoning is:
> 
> * Landlock does already work for ksmbd and cachefiles. vfs_truncate
>    does call the security_path_truncate() hook in the background.
> 
> * ksmbd and cachefiles using vfs_truncate() in kernel space is roughly
>    equivalent to a user space program using truncate(2) in a place
>    where ftruncate(2) is possible. It might not be the most elegant
>    approach, but it's legitimate to do.
> 
> * Like with any userspace program that is supposed to run under
>    Landlock, ksmbd and cachefiles both may need to be adapted slightly
>    to work well with Landlock enforcement. It is up to the person
>    adding the Landlock enforcement to double check that the program
>    works correctly under the enforced ruleset. This is true for both
>    programs running in user space and kernel space.
> 
> So yes, to run ksmbd and cachefiles under Landlock, we may need to
> extract a fs/open.c:vfs_ftruncate() in addition to vfs_truncate(), but
> I don't think it should be part of this patch set.
> 
> So my proposal would be to:
> 
> * not do the ksmbd and cachefiles changes now,
> 
> * but leave them for later when someone actually tries to run ksmbd or
>    cachefiles under Landlock.
> 
> If these components never get executed in a Landlocked context, all
> the better - we can spare ourselves a more complicated refactoring in
> a core part of the kernel.

 From my understanding, ksmbd should be treated as a process, but 
without file descriptors, which excludes it from calling ftruncate-like 
interfaces. Furthermore, I think ksmbd cannot be sandboxed because it 
calls prepare_kernel_cred(NULL) which then uses init_cred.

As a side node, using current_user_ns() in this context looks like a 
bug… I think it should be &init_user_ns instead. Any though Namjae, 
Steve or Hyunchul?

About cachefiles, I think it should be OK to ignore it, but I'd really 
like to get some input from file system folks. Any though David or 
Christian?


> 
> FWIW, I've played around with it yesterday and found that the change
> to extract a new "vfs_ftruncate()" next to vfs_truncate() is
> reasonably self-contained. But I'm not a file system expert either,
> it's well possible that I'm overlooking something.
> 
> Let me know what you think!
> 
>> On 08/09/2022 22:28, Günther Noack wrote:
>>> Adding Namjae Jeon and David Howells as authors of the respective
>>> files in fs/ksmbd and fs/cachefiles -- do you happen to know whether
>>> these vfs_truncate() calls are using 'struct file's that are opened by
>>> normal userspace processes, where LSM policies may apply?
>>>
>>> P.S. In this patch I have looked for all places where the
>>> security_path_truncate() hook was called, to see which of these should
>>> rather use security_file_truncate() (and I made sure that it does the
>>> same thing for all the LSMs that use it).
>>>
>>> I'm confident that this does the right thing when truncate() or
>>> ftruncate() are called from userspace, but one of the places that
>>> still calls the path-based hook is vfs_truncate(), and this is called
>>> from more places in the kernel than just from userspace:
>>>
>>> init/initramfs.c
>>> 387:				vfs_truncate(&wfile->f_path, body_len);
>>>
>>> security/keys/big_key.c
>>> 172:		vfs_truncate(&payload->path, 0);
>>>
>>> fs/cachefiles/interface.c
>>> 242:		ret = vfs_truncate(&file->f_path, dio_size);
>>>
>>> fs/cachefiles/namei.c
>>> 497:			ret = vfs_truncate(&path, ni_size); >
>>> fs/ksmbd/smb2pdu.c
>>> 2350:	int rc = vfs_truncate(path, 0);
>>>
>>> fs/ksmbd/vfs.c
>>> 874:	err = vfs_truncate(&filp->f_path, size);
>>>
>>> I suspect that these are benign but am not familiar with all of these
>>> corners of the codebase. -- The question is: Some of these call
>>> vfs_truncate() on the f_path of an existing struct file -- should
>>> these rather be calling the security_file_truncate() than the
>>> security_path_truncate() hook to authorize the truncation?
>>>
>>> Specifically, I think:
>>>
>>> * initramfs happens at system startup and LSMs should not interfere at
>>>     this point yet
>>> * security/keys does not use an opened struct file, so calling the
>>>     path-based hook through vfs_truncate() is correct
>>> * fs/cachefiles and fs/ksmbd use the file system from the kernel to
>>>     expose it as another file system (in a cached form for cachefiles,
>>>     and over the network for ksmbd). I suspect that these file systems
>>>     are not handling 'struct file's which are opened in contexts where a
>>>     LSM applies? It that a reasonable assumption?
>>
>> I think you're right but I have some doubts about the cachefiles subsystem.
>> I don't know how ksmb deals with these file descriptors but changing such
>> call sites (where there is a struct file) could improve API consistency
>> though.
>> Any though?
> 
> My conclusion is already summarized above, and I've tried to abstract
> away from the concrete use cases. For completeness, I've also looked
> into ksmbd and cachefiles specifically though so see whether
> security_path_truncate and security_file_truncate would make a
> difference.
> 
> For ksmbd, I strongly suspect it does not make a difference (90%
> confidence) -- the files are getting opened by the same request
> handler context which is also truncating the files later on behalf of
> a truncation operation in the SMB protocol. It's anyway unclear to me
> whether the kernel tasks executing this can be put under Landlock
> enforcement at all..?
> 
> fs/cachefiles is a more layered system and uses some
> cachefiles-independent caching structures with void* pointers, whose
> values I found difficult to trace. I'm less certain about this one as
> well, but as discussed above, it does not make a difference as long as
> none of the cachefiles code executes in a Landlock context. I'm still
> in favor of decoupling potential ksmbd and cachefiles changes from
> this patch set.
> 
> —Günther
> 
>>
>>
>>>
>>> Thanks,
>>> Günther
>>>
>>> On Thu, Sep 08, 2022 at 09:58:01PM +0200, Günther Noack wrote:
>>>> Like path_truncate, the file_truncate hook also restricts file
>>>> truncation, but is called in the cases where truncation is attempted
>>>> on an already-opened file.
>>>>
>>>> This is required in a subsequent commit to handle ftruncate()
>>>> operations differently to truncate() operations.
>>>>
>>>> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
>>>> ---
>>>>    fs/namei.c                    |  6 +++---
>>>>    fs/open.c                     |  4 ++--
>>>>    include/linux/lsm_hook_defs.h |  1 +
>>>>    include/linux/security.h      |  6 ++++++
>>>>    security/apparmor/lsm.c       |  6 ++++++
>>>>    security/security.c           |  5 +++++
>>>>    security/tomoyo/tomoyo.c      | 13 +++++++++++++
>>>>    7 files changed, 36 insertions(+), 5 deletions(-)
>>>>
>>>> diff --git a/fs/namei.c b/fs/namei.c
>>>> index 53b4bc094db2..52105873d1f8 100644
>>>> --- a/fs/namei.c
>>>> +++ b/fs/namei.c
>>>> @@ -53,8 +53,8 @@
>>>>     * The new code replaces the old recursive symlink resolution with
>>>>     * an iterative one (in case of non-nested symlink chains).  It does
>>>>     * this with calls to <fs>_follow_link().
>>>> - * As a side effect, dir_namei(), _namei() and follow_link() are now
>>>> - * replaced with a single function lookup_dentry() that can handle all
>>>> + * As a side effect, dir_namei(), _namei() and follow_link() are now
>>>> + * replaced with a single function lookup_dentry() that can handle all
>>>>     * the special cases of the former code.
>>>>     *
>>>>     * With the new dcache, the pathname is stored at each inode, at least as
>>>> @@ -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 8a813fa5ca56..0831433e493a 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);
>>>> @@ -1271,7 +1271,7 @@ struct file *filp_open(const char *filename, int flags, umode_t mode)
>>>>    {
>>>>    	struct filename *name = getname_kernel(filename);
>>>>    	struct file *file = ERR_CAST(name);
>>>> -
>>>> +
>>>>    	if (!IS_ERR(name)) {
>>>>    		file = file_open_name(name, flags, mode);
>>>>    		putname(name);
>>>> diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
>>>> index 60fff133c0b1..dee35ab253ba 100644
>>>> --- a/include/linux/lsm_hook_defs.h
>>>> +++ b/include/linux/lsm_hook_defs.h
>>>> @@ -177,6 +177,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/security.h b/include/linux/security.h
>>>> index 7bd0c490703d..f80b23382dd9 100644
>>>> --- a/include/linux/security.h
>>>> +++ b/include/linux/security.h
>>>> @@ -394,6 +394,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);
>>>> @@ -1011,6 +1012,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/security/apparmor/lsm.c b/security/apparmor/lsm.c
>>>> index e29cade7b662..98ecb7f221b8 100644
>>>> --- a/security/apparmor/lsm.c
>>>> +++ b/security/apparmor/lsm.c
>>>> @@ -329,6 +329,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)
>>>>    {
>>>> @@ -1232,6 +1237,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/security.c b/security/security.c
>>>> index 4b95de24bc8d..e491120c48ba 100644
>>>> --- a/security/security.c
>>>> +++ b/security/security.c
>>>> @@ -1210,6 +1210,11 @@ int security_path_truncate(const struct path *path)
>>>>    	return call_int_hook(path_truncate, 0, path);
>>>>    }
>>>>
>>>> +int security_file_truncate(struct file *file)
>>>> +{
>>>> +	return call_int_hook(file_truncate, 0, file);
>>>> +}
>>>> +
>>>>    int security_path_chmod(const struct path *path, umode_t mode)
>>>>    {
>>>>    	if (unlikely(IS_PRIVATE(d_backing_inode(path->dentry))))
>>>> 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
>>>> @@ -134,6 +134,18 @@ static int tomoyo_path_truncate(const struct path *path)
>>>>    	return tomoyo_path_perm(TOMOYO_TYPE_TRUNCATE, path, NULL);
>>>>    }
>>>>
>>>> +/**
>>>> + * 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().
>>>>     *
>>>> @@ -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),
>>>> --
>>>> 2.37.3
>>>>
>>>
>>> --
> 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook
  2022-09-28 20:04         ` Mickaël Salaün
@ 2022-09-29  2:55           ` Namjae Jeon
  0 siblings, 0 replies; 39+ messages in thread
From: Namjae Jeon @ 2022-09-29  2:55 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Günther Noack, David Howells, Christian Brauner,
	Steve French, Hyunchul Lee, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze,
	linux-security-module, linux-cifs, Al Viro

2022-09-29 5:04 GMT+09:00, Mickaël Salaün <mic@digikod.net>:
>
> On 26/09/2022 18:07, Günther Noack wrote:
>> On Fri, Sep 16, 2022 at 07:30:24PM +0200, Mickaël Salaün wrote:
>>> We may indeed need to change fs/open.c:vfs_truncate() because of these
>>> different call sites. I'm not sure how these subsystems work though.
>>
>> I thought about this some more, and I'm coming around to the
>> conclusion that we should not block the truncate patch set on changes
>> in ksmbd and cachefiles.
>>
>> The reasoning is:
>>
>> * Landlock does already work for ksmbd and cachefiles. vfs_truncate
>>    does call the security_path_truncate() hook in the background.
>>
>> * ksmbd and cachefiles using vfs_truncate() in kernel space is roughly
>>    equivalent to a user space program using truncate(2) in a place
>>    where ftruncate(2) is possible. It might not be the most elegant
>>    approach, but it's legitimate to do.
>>
>> * Like with any userspace program that is supposed to run under
>>    Landlock, ksmbd and cachefiles both may need to be adapted slightly
>>    to work well with Landlock enforcement. It is up to the person
>>    adding the Landlock enforcement to double check that the program
>>    works correctly under the enforced ruleset. This is true for both
>>    programs running in user space and kernel space.
>>
>> So yes, to run ksmbd and cachefiles under Landlock, we may need to
>> extract a fs/open.c:vfs_ftruncate() in addition to vfs_truncate(), but
>> I don't think it should be part of this patch set.
>>
>> So my proposal would be to:
>>
>> * not do the ksmbd and cachefiles changes now,
>>
>> * but leave them for later when someone actually tries to run ksmbd or
>>    cachefiles under Landlock.
>>
>> If these components never get executed in a Landlocked context, all
>> the better - we can spare ourselves a more complicated refactoring in
>> a core part of the kernel.
>
>  From my understanding, ksmbd should be treated as a process, but
> without file descriptors, which excludes it from calling ftruncate-like
> interfaces. Furthermore, I think ksmbd cannot be sandboxed because it
> calls prepare_kernel_cred(NULL) which then uses init_cred.
>
> As a side node, using current_user_ns() in this context looks like a
> bug… I think it should be &init_user_ns instead. Any though Namjae,
> Steve or Hyunchul?
Agreed, Could you please send the patch for this to the list ?

Thanks!
>
> About cachefiles, I think it should be OK to ignore it, but I'd really
> like to get some input from file system folks. Any though David or
> Christian?
>
>
>>
>> FWIW, I've played around with it yesterday and found that the change
>> to extract a new "vfs_ftruncate()" next to vfs_truncate() is
>> reasonably self-contained. But I'm not a file system expert either,
>> it's well possible that I'm overlooking something.
>>
>> Let me know what you think!
>>
>>> On 08/09/2022 22:28, Günther Noack wrote:
>>>> Adding Namjae Jeon and David Howells as authors of the respective
>>>> files in fs/ksmbd and fs/cachefiles -- do you happen to know whether
>>>> these vfs_truncate() calls are using 'struct file's that are opened by
>>>> normal userspace processes, where LSM policies may apply?
>>>>
>>>> P.S. In this patch I have looked for all places where the
>>>> security_path_truncate() hook was called, to see which of these should
>>>> rather use security_file_truncate() (and I made sure that it does the
>>>> same thing for all the LSMs that use it).
>>>>
>>>> I'm confident that this does the right thing when truncate() or
>>>> ftruncate() are called from userspace, but one of the places that
>>>> still calls the path-based hook is vfs_truncate(), and this is called
>>>> from more places in the kernel than just from userspace:
>>>>
>>>> init/initramfs.c
>>>> 387:				vfs_truncate(&wfile->f_path, body_len);
>>>>
>>>> security/keys/big_key.c
>>>> 172:		vfs_truncate(&payload->path, 0);
>>>>
>>>> fs/cachefiles/interface.c
>>>> 242:		ret = vfs_truncate(&file->f_path, dio_size);
>>>>
>>>> fs/cachefiles/namei.c
>>>> 497:			ret = vfs_truncate(&path, ni_size); >
>>>> fs/ksmbd/smb2pdu.c
>>>> 2350:	int rc = vfs_truncate(path, 0);
>>>>
>>>> fs/ksmbd/vfs.c
>>>> 874:	err = vfs_truncate(&filp->f_path, size);
>>>>
>>>> I suspect that these are benign but am not familiar with all of these
>>>> corners of the codebase. -- The question is: Some of these call
>>>> vfs_truncate() on the f_path of an existing struct file -- should
>>>> these rather be calling the security_file_truncate() than the
>>>> security_path_truncate() hook to authorize the truncation?
>>>>
>>>> Specifically, I think:
>>>>
>>>> * initramfs happens at system startup and LSMs should not interfere at
>>>>     this point yet
>>>> * security/keys does not use an opened struct file, so calling the
>>>>     path-based hook through vfs_truncate() is correct
>>>> * fs/cachefiles and fs/ksmbd use the file system from the kernel to
>>>>     expose it as another file system (in a cached form for cachefiles,
>>>>     and over the network for ksmbd). I suspect that these file systems
>>>>     are not handling 'struct file's which are opened in contexts where
>>>> a
>>>>     LSM applies? It that a reasonable assumption?
>>>
>>> I think you're right but I have some doubts about the cachefiles
>>> subsystem.
>>> I don't know how ksmb deals with these file descriptors but changing
>>> such
>>> call sites (where there is a struct file) could improve API consistency
>>> though.
>>> Any though?
>>
>> My conclusion is already summarized above, and I've tried to abstract
>> away from the concrete use cases. For completeness, I've also looked
>> into ksmbd and cachefiles specifically though so see whether
>> security_path_truncate and security_file_truncate would make a
>> difference.
>>
>> For ksmbd, I strongly suspect it does not make a difference (90%
>> confidence) -- the files are getting opened by the same request
>> handler context which is also truncating the files later on behalf of
>> a truncation operation in the SMB protocol. It's anyway unclear to me
>> whether the kernel tasks executing this can be put under Landlock
>> enforcement at all..?
>>
>> fs/cachefiles is a more layered system and uses some
>> cachefiles-independent caching structures with void* pointers, whose
>> values I found difficult to trace. I'm less certain about this one as
>> well, but as discussed above, it does not make a difference as long as
>> none of the cachefiles code executes in a Landlock context. I'm still
>> in favor of decoupling potential ksmbd and cachefiles changes from
>> this patch set.
>>
>> —Günther
>>
>>>
>>>
>>>>
>>>> Thanks,
>>>> Günther
>>>>
>>>> On Thu, Sep 08, 2022 at 09:58:01PM +0200, Günther Noack wrote:
>>>>> Like path_truncate, the file_truncate hook also restricts file
>>>>> truncation, but is called in the cases where truncation is attempted
>>>>> on an already-opened file.
>>>>>
>>>>> This is required in a subsequent commit to handle ftruncate()
>>>>> operations differently to truncate() operations.
>>>>>
>>>>> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
>>>>> ---
>>>>>    fs/namei.c                    |  6 +++---
>>>>>    fs/open.c                     |  4 ++--
>>>>>    include/linux/lsm_hook_defs.h |  1 +
>>>>>    include/linux/security.h      |  6 ++++++
>>>>>    security/apparmor/lsm.c       |  6 ++++++
>>>>>    security/security.c           |  5 +++++
>>>>>    security/tomoyo/tomoyo.c      | 13 +++++++++++++
>>>>>    7 files changed, 36 insertions(+), 5 deletions(-)
>>>>>
>>>>> diff --git a/fs/namei.c b/fs/namei.c
>>>>> index 53b4bc094db2..52105873d1f8 100644
>>>>> --- a/fs/namei.c
>>>>> +++ b/fs/namei.c
>>>>> @@ -53,8 +53,8 @@
>>>>>     * The new code replaces the old recursive symlink resolution with
>>>>>     * an iterative one (in case of non-nested symlink chains).  It
>>>>> does
>>>>>     * this with calls to <fs>_follow_link().
>>>>> - * As a side effect, dir_namei(), _namei() and follow_link() are now
>>>>> - * replaced with a single function lookup_dentry() that can handle
>>>>> all
>>>>> + * As a side effect, dir_namei(), _namei() and follow_link() are now
>>>>> + * replaced with a single function lookup_dentry() that can handle
>>>>> all
>>>>>     * the special cases of the former code.
>>>>>     *
>>>>>     * With the new dcache, the pathname is stored at each inode, at
>>>>> least as
>>>>> @@ -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 8a813fa5ca56..0831433e493a 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);
>>>>> @@ -1271,7 +1271,7 @@ struct file *filp_open(const char *filename, int
>>>>> flags, umode_t mode)
>>>>>    {
>>>>>    	struct filename *name = getname_kernel(filename);
>>>>>    	struct file *file = ERR_CAST(name);
>>>>> -
>>>>> +
>>>>>    	if (!IS_ERR(name)) {
>>>>>    		file = file_open_name(name, flags, mode);
>>>>>    		putname(name);
>>>>> diff --git a/include/linux/lsm_hook_defs.h
>>>>> b/include/linux/lsm_hook_defs.h
>>>>> index 60fff133c0b1..dee35ab253ba 100644
>>>>> --- a/include/linux/lsm_hook_defs.h
>>>>> +++ b/include/linux/lsm_hook_defs.h
>>>>> @@ -177,6 +177,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/security.h b/include/linux/security.h
>>>>> index 7bd0c490703d..f80b23382dd9 100644
>>>>> --- a/include/linux/security.h
>>>>> +++ b/include/linux/security.h
>>>>> @@ -394,6 +394,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);
>>>>> @@ -1011,6 +1012,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/security/apparmor/lsm.c b/security/apparmor/lsm.c
>>>>> index e29cade7b662..98ecb7f221b8 100644
>>>>> --- a/security/apparmor/lsm.c
>>>>> +++ b/security/apparmor/lsm.c
>>>>> @@ -329,6 +329,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)
>>>>>    {
>>>>> @@ -1232,6 +1237,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/security.c b/security/security.c
>>>>> index 4b95de24bc8d..e491120c48ba 100644
>>>>> --- a/security/security.c
>>>>> +++ b/security/security.c
>>>>> @@ -1210,6 +1210,11 @@ int security_path_truncate(const struct path
>>>>> *path)
>>>>>    	return call_int_hook(path_truncate, 0, path);
>>>>>    }
>>>>>
>>>>> +int security_file_truncate(struct file *file)
>>>>> +{
>>>>> +	return call_int_hook(file_truncate, 0, file);
>>>>> +}
>>>>> +
>>>>>    int security_path_chmod(const struct path *path, umode_t mode)
>>>>>    {
>>>>>    	if (unlikely(IS_PRIVATE(d_backing_inode(path->dentry))))
>>>>> 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
>>>>> @@ -134,6 +134,18 @@ static int tomoyo_path_truncate(const struct path
>>>>> *path)
>>>>>    	return tomoyo_path_perm(TOMOYO_TYPE_TRUNCATE, path, NULL);
>>>>>    }
>>>>>
>>>>> +/**
>>>>> + * 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().
>>>>>     *
>>>>> @@ -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),
>>>>> --
>>>>> 2.37.3
>>>>>
>>>>
>>>> --
>>
>

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/5] landlock: Support file truncation
  2022-09-28 18:32           ` Mickaël Salaün
@ 2022-09-29 19:22             ` Günther Noack
  2022-09-30 15:56               ` Mickaël Salaün
  0 siblings, 1 reply; 39+ messages in thread
From: Günther Noack @ 2022-09-29 19:22 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze

On Wed, Sep 28, 2022 at 08:32:02PM +0200, Mickaël Salaün wrote:
> On 25/09/2022 20:09, Günther Noack wrote:
> > On Fri, Sep 23, 2022 at 10:53:23PM +0200, Mickaël Salaün wrote:
> > > On 23/09/2022 13:21, Günther Noack wrote:
> > > > On Mon, Sep 12, 2022 at 09:41:32PM +0200, Mickaël Salaün wrote:
> > > > > On 08/09/2022 21:58, Günther Noack wrote:
> > > > > > Introduce the LANDLOCK_ACCESS_FS_TRUNCATE flag for file truncation.
> > > > > 
> > > > > [...]
> > > > > 
> > > > > > +/**
> > > > > > + * get_path_access_rights - Returns the subset of rights in access_request
> > > > > > + * which are permitted for the given path.
> > > > > > + *
> > > > > > + * @domain: The domain that defines the current restrictions.
> > > > > > + * @path: The path to get access rights for.
> > > > > > + * @access_request: The rights we are interested in.
> > > > > > + *
> > > > > > + * Returns: The access mask of the rights that are permitted on the given path,
> > > > > > + * which are also a subset of access_request (to save some calculation time).
> > > > > > + */
> > > > > > +static inline access_mask_t
> > > > > > +get_path_access_rights(const struct landlock_ruleset *const domain,
> > > > > > +		       const struct path *const path,
> > > > > > +		       access_mask_t access_request)
> > > > > > +{
> > > > > > +	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
> > > > > > +	unsigned long access_bit;
> > > > > > +	unsigned long access_req;
> > > > > > +
> > > > > > +	init_layer_masks(domain, access_request, &layer_masks);
> > > > > > +	if (!check_access_path_dual(domain, path, access_request, &layer_masks,
> > > > > > +				    NULL, 0, NULL, NULL)) {
> > > > > > +		/*
> > > > > > +		 * Return immediately for successful accesses and for cases
> > > > > > +		 * where everything is permitted because the path belongs to an
> > > > > > +		 * internal filesystem.
> > > > > > +		 */
> > > > > > +		return access_request;
> > > > > > +	}
> > > > > > +
> > > > > > +	access_req = access_request;
> > > > > > +	for_each_set_bit(access_bit, &access_req, ARRAY_SIZE(layer_masks)) {
> > > > > > +		if (layer_masks[access_bit]) {
> > > > > > +			/* If any layer vetoed the access right, remove it. */
> > > > > > +			access_request &= ~BIT_ULL(access_bit);
> > > > > > +		}
> > > > > > +	}
> > > > > 
> > > > > This seems to be redundant with the value returned by init_layer_masks(),
> > > > > which should be passed to check_access_path_dual() to avoid useless path
> > > > > walk.
> > > > 
> > > > True, I'll use the result of init_layer_masks() to feed it back to
> > > > check_access_path_dual() to avoid a bit of computation.
> > > > 
> > > > Like this:
> > > > 
> > > >           effective_access_request =
> > > > 		init_layer_masks(domain, access_request, &layer_masks);
> > > > 	if (!check_access_path_dual(domain, path, effective_access_request,
> > > > 	    &layer_masks, NULL, 0, NULL, NULL)) {
> > > > 		// ...
> > > > 	}
> > > 
> > > correct
> > > 
> > > > 
> > > > Overall, the approach here is:
> > > > 
> > > > * Initialize the layer_masks, so that it has a bit set for every
> > > >     access right in access_request and layer where that access right is
> > > >     handled.
> > > > 
> > > > * check_access_path_dual() with only the first few parameters -- this
> > > >     will clear all the bits in layer masks which are actually permitted
> > > >     according to the individual rules.
> > > > 
> > > >     As a special case, this *may* return 0 immediately, in which case we
> > > >     can (a) save a bit of calculation in the loop below and (b) we might
> > > >     be in the case where access is permitted because it's a file from a
> > > >     special file system (even though not all bits are cleared). If
> > > >     check_access_path_dual() returns 0, we return the full requested
> > > >     access_request that we received as input. >
> > > > * In the loop below, if there are any bits left in layer_masks, those
> > > >     are rights which are not permitted for the given path. We remove
> > > >     these from access_request and return the modified access_request.
> > > > 
> > > > 
> > > > > This function is pretty similar to check_access_path(). Can't you change it
> > > > > to use an access_mask_t pointer and get almost the same thing?
> > > > 
> > > > I'm shying away from this approach. Many of the existing different use
> > > > cases are already realized by "doing if checks deep down". I think it
> > > > would make the code more understandable if we managed to model these
> > > > differences between use cases already at the layer of function calls.
> > > > (This is particularly true for check_access_path_dual(), where in
> > > > order to find out how the "single" case works, you need to disentangle
> > > > to a large extent how the much more complicated dual case works.)
> > > 
> > > I agree that check_access_path_dual() is complex, but I couldn't find a
> > > better way.
> > 
> > It seems out of the scope of this patch set, but I sometimes find it
> > OK to just duplicate the code and have a set of tests to demonstrate
> > that the two variants do the same thing.
> > 
> > check_access_path_dual() is mostly complex because of performance
> > reasons, as far as I can tell, and it might be possible to check its
> > results against a parallel implementation of it which runs slower,
> > uses more memory, but is more obviously correct. (I have used one
> > myself to check against when developing the truncate patch set.)
> > 
> > > > If you want to unify these two functions, what do you think of the
> > > > approach of just using get_path_access_rights() instead of
> > > > check_access_path()?
> > > > 
> > > > Basically, it would turn
> > > > 
> > > > return check_access_path(dom, path, access_request);
> > > > 
> > > > into
> > > > 
> > > > if (get_path_access_rights(dom, path, access_request) == access_request)
> > > > 	return 0;
> > > > return -EACCES;
> > > > 
> > > > This is slightly more verbose in the places where it's called, but it
> > > > would be more orthogonal, and it would also clarify that -EACCES is
> > > > the only possible error in the "single" path walk case.
> > > > 
> > > > Let me know what you think.
> > > 
> > > What about adding an additional argument `access_mask_t *const
> > > access_allowed` to check_access_path_dual() which returns the set of
> > > accesses (i.e. access_masked_parent1 & access_masked_parent2) that could
> > > then be stored to landlock_file(file)->allowed_access? If this argument is
> > > NULL it should just be ignored. What is left from get_path_access_rights()
> > > could then be merged into hook_file_open().
> > 
> > IMHO, check_access_path_dual() does not seem like the right place to
> > add this. This functionality is not needed in any of the "dual path"
> > cases so far, and I'm not sure what it would mean. The necessary
> > information can also be easily derived from the resulting layer_masks,
> > which is already exposed in the check_access_path_dual() interface,
> > and I also believe that this approach is at least equally fast as
> > updating it on the fly when changing the layer_masks.
> > 
> > I could be convinced to add a `access_mask_t *const access_allowed`
> > argument to check_access_path() if you prefer that, but then again, in
> > that case the returned boolean can be reconstructed from the new
> > access_allowed variable, and we could as well make check_access_path()
> > return the access_allowed result instead of the boolean and let
> > callers check equality with what they expected...? (I admittedly don't
> > have a good setup to test the performance right now, but it looks like
> > a negligible difference to me?)
> 
> Good idea, let's try to make check_access_path_dual() returns the allowed
> accesses (according to the request) and rename it to get_access_path_dual().
> unmask_layers() could be changed to return the still-denied accesses instead
> of a boolean, and we could use this values (for potential both parents) to
> return allowed_parent1 & allowed_parent2 (with access_mask_t types). This
> would also simplify is_eaccess() and its calls could be moved to
> current_check_refer_path(). This would merge get_path_access_rights() into
> check_access_path_dual() and make the errno codes more explicit per hook or
> defined in check_access_path().

Thanks for the review!

I'm afraid I don't understand this approach at the moment. I'm
probably still missing some insight about how the "refer" logic works
which would make this clearer.

With the proposed changes to check_access_path_dual(), it sounds like
we would have to change the logic of the "refer" implementation quite
a bit, which would expand the scope of the "truncate" patch set beyond
what it was originally meant to do. Is this check_access_path_dual()
refactoring something you'd insist on for the truncate patch set, or
would you be OK with doing that separately?

For the truncate patch set, what do you think of the lighter
refactoring options, which I had outlined in my previous mail? - see
the four bullet points quoted here:

> > Here are the options we have discussed, in the order that I would
> > prefer them:
> > 
> > * to keep it as a separate function as it already is,
> >    slightly duplicating check_access_path(). (I think it's cleaner,
> >    because the code path for the rest of the hooks other than
> >    security_file_open() stays simpler.)
> > 
> > * to make check_access_path() return the access_allowed access mask
> >    and make callers check that it covers the access_request that they
> >    asked for (see example from my previous mail on this thread). (This
> >    is equivalent to discarding the existing check_access_path() and
> >    using the get_path_access() function instead.)
> > 
> > * to add a `access_mask_t *const access_allowed` argument to
> >    check_access_path(), which is calculated if it's non-NULL based on
> >    the layer_masks result. It would be used from the security_file_open
> >    hook.
> > 
> > * to add a `access_mask_t *const access_allowed` argument to
> >    check_access_path_dual(). This doesn't make much sense, IMHO,
> >    because an on-the-fly calculation of this result does not look like
> >    a performance benefit to me, and calculating it based on the two
> >    resulting layer_masks is already possible now. It's also not clear
> >    to me what it would mean to calculate an access_allowed on two paths
> >    at once, and what that would be used for.
> > 
> > Let me know which option you prefer. In the end, I don't feel that
> > strongly about it and I'm happy to do this either way.

Thanks,
Günther

-- 

^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH v6 2/5] landlock: Support file truncation
  2022-09-29 19:22             ` Günther Noack
@ 2022-09-30 15:56               ` Mickaël Salaün
  0 siblings, 0 replies; 39+ messages in thread
From: Mickaël Salaün @ 2022-09-30 15:56 UTC (permalink / raw)
  To: Günther Noack
  Cc: linux-security-module, James Morris, Paul Moore,
	Serge E . Hallyn, linux-fsdevel, Konstantin Meskhidze


On 29/09/2022 21:22, Günther Noack wrote:
> On Wed, Sep 28, 2022 at 08:32:02PM +0200, Mickaël Salaün wrote:
>> On 25/09/2022 20:09, Günther Noack wrote:
>>> On Fri, Sep 23, 2022 at 10:53:23PM +0200, Mickaël Salaün wrote:
>>>> On 23/09/2022 13:21, Günther Noack wrote:
>>>>> On Mon, Sep 12, 2022 at 09:41:32PM +0200, Mickaël Salaün wrote:
>>>>>> On 08/09/2022 21:58, Günther Noack wrote:
>>>>>>> Introduce the LANDLOCK_ACCESS_FS_TRUNCATE flag for file truncation.
>>>>>>
>>>>>> [...]
>>>>>>
>>>>>>> +/**
>>>>>>> + * get_path_access_rights - Returns the subset of rights in access_request
>>>>>>> + * which are permitted for the given path.
>>>>>>> + *
>>>>>>> + * @domain: The domain that defines the current restrictions.
>>>>>>> + * @path: The path to get access rights for.
>>>>>>> + * @access_request: The rights we are interested in.
>>>>>>> + *
>>>>>>> + * Returns: The access mask of the rights that are permitted on the given path,
>>>>>>> + * which are also a subset of access_request (to save some calculation time).
>>>>>>> + */
>>>>>>> +static inline access_mask_t
>>>>>>> +get_path_access_rights(const struct landlock_ruleset *const domain,
>>>>>>> +		       const struct path *const path,
>>>>>>> +		       access_mask_t access_request)
>>>>>>> +{
>>>>>>> +	layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
>>>>>>> +	unsigned long access_bit;
>>>>>>> +	unsigned long access_req;
>>>>>>> +
>>>>>>> +	init_layer_masks(domain, access_request, &layer_masks);
>>>>>>> +	if (!check_access_path_dual(domain, path, access_request, &layer_masks,
>>>>>>> +				    NULL, 0, NULL, NULL)) {
>>>>>>> +		/*
>>>>>>> +		 * Return immediately for successful accesses and for cases
>>>>>>> +		 * where everything is permitted because the path belongs to an
>>>>>>> +		 * internal filesystem.
>>>>>>> +		 */
>>>>>>> +		return access_request;
>>>>>>> +	}
>>>>>>> +
>>>>>>> +	access_req = access_request;
>>>>>>> +	for_each_set_bit(access_bit, &access_req, ARRAY_SIZE(layer_masks)) {
>>>>>>> +		if (layer_masks[access_bit]) {
>>>>>>> +			/* If any layer vetoed the access right, remove it. */
>>>>>>> +			access_request &= ~BIT_ULL(access_bit);
>>>>>>> +		}
>>>>>>> +	}
>>>>>>
>>>>>> This seems to be redundant with the value returned by init_layer_masks(),
>>>>>> which should be passed to check_access_path_dual() to avoid useless path
>>>>>> walk.
>>>>>
>>>>> True, I'll use the result of init_layer_masks() to feed it back to
>>>>> check_access_path_dual() to avoid a bit of computation.
>>>>>
>>>>> Like this:
>>>>>
>>>>>            effective_access_request =
>>>>> 		init_layer_masks(domain, access_request, &layer_masks);
>>>>> 	if (!check_access_path_dual(domain, path, effective_access_request,
>>>>> 	    &layer_masks, NULL, 0, NULL, NULL)) {
>>>>> 		// ...
>>>>> 	}
>>>>
>>>> correct
>>>>
>>>>>
>>>>> Overall, the approach here is:
>>>>>
>>>>> * Initialize the layer_masks, so that it has a bit set for every
>>>>>      access right in access_request and layer where that access right is
>>>>>      handled.
>>>>>
>>>>> * check_access_path_dual() with only the first few parameters -- this
>>>>>      will clear all the bits in layer masks which are actually permitted
>>>>>      according to the individual rules.
>>>>>
>>>>>      As a special case, this *may* return 0 immediately, in which case we
>>>>>      can (a) save a bit of calculation in the loop below and (b) we might
>>>>>      be in the case where access is permitted because it's a file from a
>>>>>      special file system (even though not all bits are cleared). If
>>>>>      check_access_path_dual() returns 0, we return the full requested
>>>>>      access_request that we received as input. >
>>>>> * In the loop below, if there are any bits left in layer_masks, those
>>>>>      are rights which are not permitted for the given path. We remove
>>>>>      these from access_request and return the modified access_request.
>>>>>
>>>>>
>>>>>> This function is pretty similar to check_access_path(). Can't you change it
>>>>>> to use an access_mask_t pointer and get almost the same thing?
>>>>>
>>>>> I'm shying away from this approach. Many of the existing different use
>>>>> cases are already realized by "doing if checks deep down". I think it
>>>>> would make the code more understandable if we managed to model these
>>>>> differences between use cases already at the layer of function calls.
>>>>> (This is particularly true for check_access_path_dual(), where in
>>>>> order to find out how the "single" case works, you need to disentangle
>>>>> to a large extent how the much more complicated dual case works.)
>>>>
>>>> I agree that check_access_path_dual() is complex, but I couldn't find a
>>>> better way.
>>>
>>> It seems out of the scope of this patch set, but I sometimes find it
>>> OK to just duplicate the code and have a set of tests to demonstrate
>>> that the two variants do the same thing.
>>>
>>> check_access_path_dual() is mostly complex because of performance
>>> reasons, as far as I can tell, and it might be possible to check its
>>> results against a parallel implementation of it which runs slower,
>>> uses more memory, but is more obviously correct. (I have used one
>>> myself to check against when developing the truncate patch set.)
>>>
>>>>> If you want to unify these two functions, what do you think of the
>>>>> approach of just using get_path_access_rights() instead of
>>>>> check_access_path()?
>>>>>
>>>>> Basically, it would turn
>>>>>
>>>>> return check_access_path(dom, path, access_request);
>>>>>
>>>>> into
>>>>>
>>>>> if (get_path_access_rights(dom, path, access_request) == access_request)
>>>>> 	return 0;
>>>>> return -EACCES;
>>>>>
>>>>> This is slightly more verbose in the places where it's called, but it
>>>>> would be more orthogonal, and it would also clarify that -EACCES is
>>>>> the only possible error in the "single" path walk case.
>>>>>
>>>>> Let me know what you think.
>>>>
>>>> What about adding an additional argument `access_mask_t *const
>>>> access_allowed` to check_access_path_dual() which returns the set of
>>>> accesses (i.e. access_masked_parent1 & access_masked_parent2) that could
>>>> then be stored to landlock_file(file)->allowed_access? If this argument is
>>>> NULL it should just be ignored. What is left from get_path_access_rights()
>>>> could then be merged into hook_file_open().
>>>
>>> IMHO, check_access_path_dual() does not seem like the right place to
>>> add this. This functionality is not needed in any of the "dual path"
>>> cases so far, and I'm not sure what it would mean. The necessary
>>> information can also be easily derived from the resulting layer_masks,
>>> which is already exposed in the check_access_path_dual() interface,
>>> and I also believe that this approach is at least equally fast as
>>> updating it on the fly when changing the layer_masks.
>>>
>>> I could be convinced to add a `access_mask_t *const access_allowed`
>>> argument to check_access_path() if you prefer that, but then again, in
>>> that case the returned boolean can be reconstructed from the new
>>> access_allowed variable, and we could as well make check_access_path()
>>> return the access_allowed result instead of the boolean and let
>>> callers check equality with what they expected...? (I admittedly don't
>>> have a good setup to test the performance right now, but it looks like
>>> a negligible difference to me?)
>>
>> Good idea, let's try to make check_access_path_dual() returns the allowed
>> accesses (according to the request) and rename it to get_access_path_dual().
>> unmask_layers() could be changed to return the still-denied accesses instead
>> of a boolean, and we could use this values (for potential both parents) to
>> return allowed_parent1 & allowed_parent2 (with access_mask_t types). This
>> would also simplify is_eaccess() and its calls could be moved to
>> current_check_refer_path(). This would merge get_path_access_rights() into
>> check_access_path_dual() and make the errno codes more explicit per hook or
>> defined in check_access_path().
> 
> Thanks for the review!
> 
> I'm afraid I don't understand this approach at the moment. I'm
> probably still missing some insight about how the "refer" logic works
> which would make this clearer.
> 
> With the proposed changes to check_access_path_dual(), it sounds like
> we would have to change the logic of the "refer" implementation quite
> a bit, which would expand the scope of the "truncate" patch set beyond
> what it was originally meant to do. Is this check_access_path_dual()
> refactoring something you'd insist on for the truncate patch set, or
> would you be OK with doing that separately?

I'd like to avoid stacking debts and I prefer to refactor code instead, 
but I got your point. Here is another proposal closer to yours. Let's 
rename check_access_path_dual() to is_access_to_paths_allowed(), make it 
returns a boolean (allowed_parent1 && allowed_parent2), and move the 
EACCES/EXDEV logic to (only) after the second call to 
check_access_path_dual() by current_check_refer_path() (because the if 
(old_dentry->d_parent == new_dir->dentry) branch cannot return EXDEV).

check_access_path() either returns 0 or -EACCES, and we should add a 
WARN_ON_ONCE(access_request & LANDLOCK_ACCESS_FS_REFER) to make sure 
this remains correct.

The get_path_access_rights() logic can be moved to hook_file_open() to 
make it more readable.


> 
> For the truncate patch set, what do you think of the lighter
> refactoring options, which I had outlined in my previous mail? - see
> the four bullet points quoted here:
> 
>>> Here are the options we have discussed, in the order that I would
>>> prefer them:
>>>
>>> * to keep it as a separate function as it already is,
>>>     slightly duplicating check_access_path(). (I think it's cleaner,
>>>     because the code path for the rest of the hooks other than
>>>     security_file_open() stays simpler.)
>>>
>>> * to make check_access_path() return the access_allowed access mask
>>>     and make callers check that it covers the access_request that they
>>>     asked for (see example from my previous mail on this thread). (This
>>>     is equivalent to discarding the existing check_access_path() and
>>>     using the get_path_access() function instead.)
>>>
>>> * to add a `access_mask_t *const access_allowed` argument to
>>>     check_access_path(), which is calculated if it's non-NULL based on
>>>     the layer_masks result. It would be used from the security_file_open
>>>     hook.
>>>
>>> * to add a `access_mask_t *const access_allowed` argument to
>>>     check_access_path_dual(). This doesn't make much sense, IMHO,
>>>     because an on-the-fly calculation of this result does not look like
>>>     a performance benefit to me, and calculating it based on the two
>>>     resulting layer_masks is already possible now. It's also not clear
>>>     to me what it would mean to calculate an access_allowed on two paths
>>>     at once, and what that would be used for.
>>>
>>> Let me know which option you prefer. In the end, I don't feel that
>>> strongly about it and I'm happy to do this either way.
> 
> Thanks,
> Günther
> 

^ permalink raw reply	[flat|nested] 39+ messages in thread

end of thread, other threads:[~2022-09-30 15:56 UTC | newest]

Thread overview: 39+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-09-08 19:58 [PATCH v6 0/5] landlock: truncate support Günther Noack
2022-09-08 19:58 ` [PATCH v6 1/5] security: create file_truncate hook from path_truncate hook Günther Noack
2022-09-08 20:09   ` Paul Moore
2022-09-08 20:50     ` Günther Noack
2022-09-08 22:04       ` Tetsuo Handa
2022-09-08 20:28   ` Günther Noack
2022-09-16 17:30     ` Mickaël Salaün
2022-09-26 16:07       ` Günther Noack
2022-09-28 20:04         ` Mickaël Salaün
2022-09-29  2:55           ` Namjae Jeon
2022-09-09  3:37   ` John Johansen
2022-09-09 13:50   ` Mickaël Salaün
2022-09-08 19:58 ` [PATCH v6 2/5] landlock: Support file truncation Günther Noack
2022-09-09 13:51   ` Mickaël Salaün
2022-09-12 15:28     ` Günther Noack
2022-09-12 18:37       ` Mickaël Salaün
2022-09-12 19:04         ` Günther Noack
2022-09-12 19:41   ` Mickaël Salaün
2022-09-23 11:21     ` Günther Noack
2022-09-23 20:53       ` Mickaël Salaün
2022-09-25 18:09         ` Günther Noack
2022-09-28 18:32           ` Mickaël Salaün
2022-09-29 19:22             ` Günther Noack
2022-09-30 15:56               ` Mickaël Salaün
2022-09-08 19:58 ` [PATCH v6 3/5] selftests/landlock: Selftests for file truncation support Günther Noack
2022-09-16 17:05   ` Mickaël Salaün
2022-09-23 17:50     ` Günther Noack
2022-09-23 20:54       ` Mickaël Salaün
2022-09-25 18:10         ` Günther Noack
2022-09-08 19:58 ` [PATCH v6 4/5] samples/landlock: Extend sample tool to support LANDLOCK_ACCESS_FS_TRUNCATE Günther Noack
2022-09-12 19:05   ` Mickaël Salaün
2022-09-12 19:07     ` Günther Noack
2022-09-08 19:58 ` [PATCH v6 5/5] landlock: Document Landlock's file truncation support Günther Noack
2022-09-09 13:51   ` Mickaël Salaün
2022-09-12 15:46     ` Günther Noack
2022-09-12 17:47       ` Mickaël Salaün
2022-09-12 19:05         ` Günther Noack
2022-09-12 19:15   ` Mickaël Salaün
2022-09-23 11:30     ` Günther Noack

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.