linux-kernel.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: "Mickaël Salaün" <mic@digikod.net>
To: James Morris <jmorris@namei.org>, Jann Horn <jannh@google.com>,
	"Serge E . Hallyn" <serge@hallyn.com>
Cc: "Mickaël Salaün" <mic@digikod.net>,
	"Shuah Khan" <shuah@kernel.org>,
	"Vincent Dagonneau" <vincent.dagonneau@ssi.gouv.fr>,
	linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org,
	linux-kselftest@vger.kernel.org,
	linux-security-module@vger.kernel.org
Subject: [PATCH v1 3/9] landlock: Enforce deterministic interleaved path rules
Date: Wed, 11 Nov 2020 22:34:36 +0100	[thread overview]
Message-ID: <20201111213442.434639-4-mic@digikod.net> (raw)
In-Reply-To: <20201111213442.434639-1-mic@digikod.net>

To have consistent layered rules, granting access to a path implies that
all accesses tied to inodes, from the requested file to the real root,
must be checked.  Otherwise, stacked rules may result to overzealous
restrictions.  By excluding the ability to add exceptions in the same
layer (e.g. /a allowed, /a/b denied, and /a/b/c allowed), we get
deterministic interleaved path rules.  This removes an optimization
which could be replaced by a proper cache mechanism.  This also further
simplifies and explain check_access_path_continue().

Add a layout1.interleaved_masked_accesses test to check corner-case
layered rule combinations.

Cc: James Morris <jmorris@namei.org>
Cc: Jann Horn <jannh@google.com>
Cc: Serge E. Hallyn <serge@hallyn.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
 security/landlock/fs.c                     | 38 +++++----
 tools/testing/selftests/landlock/fs_test.c | 95 ++++++++++++++++++++++
 2 files changed, 115 insertions(+), 18 deletions(-)

diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 33fc7ae17c7f..2ca4dce1e9ed 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -180,16 +180,24 @@ static bool check_access_path_continue(
 			rcu_dereference(landlock_inode(inode)->object));
 	rcu_read_unlock();
 
-	/* Checks for matching layers. */
-	if (rule && (rule->layers | *layer_mask)) {
-		if ((rule->access & access_request) == access_request) {
-			*layer_mask &= ~rule->layers;
-			return true;
-		} else {
-			return false;
-		}
+	if (!rule)
+		/* Continues to walk if there is no rule for this inode. */
+		return true;
+	/*
+	 * We must check all layers for each inode because we may encounter
+	 * multiple different accesses from the same layer in a walk.  Each
+	 * layer must at least allow the access request one time (i.e. with one
+	 * inode).  This enables to have a deterministic behavior whatever
+	 * inode is tagged within interleaved layers.
+	 */
+	if ((rule->access & access_request) == access_request) {
+		/* Validates layers for which all accesses are allowed. */
+		*layer_mask &= ~rule->layers;
+		/* Continues to walk until all layers are validated. */
+		return true;
 	}
-	return true;
+	/* Stops if a rule in the path don't allow all requested access. */
+	return false;
 }
 
 static int check_access_path(const struct landlock_ruleset *const domain,
@@ -231,12 +239,6 @@ static int check_access_path(const struct landlock_ruleset *const domain,
 				&layer_mask)) {
 		struct dentry *parent_dentry;
 
-		/* Stops when a rule from each layer granted access. */
-		if (layer_mask == 0) {
-			allowed = true;
-			break;
-		}
-
 jump_up:
 		/*
 		 * Does not work with orphaned/private mounts like overlayfs
@@ -248,10 +250,10 @@ static int check_access_path(const struct landlock_ruleset *const domain,
 				goto jump_up;
 			} else {
 				/*
-				 * Stops at the real root.  Denies access
-				 * because not all layers have granted access.
+				 * Stops at the real root.  Denies access if
+				 * not all layers granted access.
 				 */
-				allowed = false;
+				allowed = (layer_mask == 0);
 				break;
 			}
 		}
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 8aed28081ec8..ade0ad8728d8 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -629,6 +629,101 @@ TEST_F(layout1, ruleset_overlap)
 	EXPECT_EQ(0, close(open_fd));
 }
 
+TEST_F(layout1, interleaved_masked_accesses)
+{
+	/*
+	 * Checks overly restrictive rules:
+	 * layer 1: allows s1d1/s1d2/s1d3/file1
+	 * layer 2: allows s1d1/s1d2/s1d3
+	 *          denies s1d1/s1d2
+	 * layer 3: allows s1d1
+	 * layer 4: allows s1d1/s1d2
+	 */
+	const struct rule layer1[] = {
+		/* Allows access to file1_s1d3 with the first layer. */
+		{
+			.path = file1_s1d3,
+			.access = LANDLOCK_ACCESS_FS_READ_FILE,
+		},
+		{}
+	};
+	const struct rule layer2[] = {
+		/* Start by granting access to file1_s1d3 with this rule... */
+		{
+			.path = dir_s1d3,
+			.access = LANDLOCK_ACCESS_FS_READ_FILE,
+		},
+		/* ...but finally denies access to file1_s1d3. */
+		{
+			.path = dir_s1d2,
+			.access = 0,
+		},
+		{}
+	};
+	const struct rule layer3[] = {
+		/* Try to allows access to file1_s1d3. */
+		{
+			.path = dir_s1d1,
+			.access = LANDLOCK_ACCESS_FS_READ_FILE,
+		},
+		{}
+	};
+	const struct rule layer4[] = {
+		/* Try to bypass layer2. */
+		{
+			.path = dir_s1d2,
+			.access = LANDLOCK_ACCESS_FS_READ_FILE,
+		},
+		{}
+	};
+	int open_fd, ruleset_fd;
+
+	ruleset_fd = create_ruleset(_metadata, ACCESS_RO, layer1);
+	ASSERT_LE(0, ruleset_fd);
+	enforce_ruleset(_metadata, ruleset_fd);
+	EXPECT_EQ(0, close(ruleset_fd));
+
+	/* Checks that access is granted for file1_s1d3. */
+	open_fd = open(file1_s1d3, O_RDONLY | O_CLOEXEC);
+	ASSERT_LE(0, open_fd);
+	EXPECT_EQ(0, close(open_fd));
+	ASSERT_EQ(-1, open(file2_s1d3, O_RDONLY | O_CLOEXEC));
+	ASSERT_EQ(EACCES, errno);
+
+	ruleset_fd = create_ruleset(_metadata, ACCESS_RO, layer2);
+	ASSERT_LE(0, ruleset_fd);
+	enforce_ruleset(_metadata, ruleset_fd);
+	EXPECT_EQ(0, close(ruleset_fd));
+
+	/* Now, checks that access is denied for file1_s1d3. */
+	ASSERT_EQ(-1, open(file1_s1d3, O_RDONLY | O_CLOEXEC));
+	ASSERT_EQ(EACCES, errno);
+	ASSERT_EQ(-1, open(file2_s1d3, O_RDONLY | O_CLOEXEC));
+	ASSERT_EQ(EACCES, errno);
+
+	ruleset_fd = create_ruleset(_metadata, ACCESS_RO, layer3);
+	ASSERT_LE(0, ruleset_fd);
+	enforce_ruleset(_metadata, ruleset_fd);
+	EXPECT_EQ(0, close(ruleset_fd));
+
+	/* Checks that access rights are unchanged with layer 3. */
+	ASSERT_EQ(-1, open(file1_s1d3, O_RDONLY | O_CLOEXEC));
+	ASSERT_EQ(EACCES, errno);
+	ASSERT_EQ(-1, open(file2_s1d3, O_RDONLY | O_CLOEXEC));
+	ASSERT_EQ(EACCES, errno);
+
+	ruleset_fd = create_ruleset(_metadata, ACCESS_RO, layer4);
+	ASSERT_LE(0, ruleset_fd);
+	enforce_ruleset(_metadata, ruleset_fd);
+	EXPECT_EQ(0, close(ruleset_fd));
+
+	/* Checks that access rights are unchanged with layer 4. */
+	ASSERT_EQ(-1, open(file1_s1d3, O_RDONLY | O_CLOEXEC));
+	ASSERT_EQ(EACCES, errno);
+	ASSERT_EQ(-1, open(file2_s1d3, O_RDONLY | O_CLOEXEC));
+	ASSERT_EQ(EACCES, errno);
+}
+
 TEST_F(layout1, inherit_subset)
 {
 	const struct rule rules[] = {
-- 
2.29.2


  parent reply	other threads:[~2020-11-11 21:35 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-11-11 21:34 [PATCH v1 0/9] Landlock fixes Mickaël Salaün
2020-11-11 21:34 ` [PATCH v1 1/9] landlock: Fix memory allocation error handling Mickaël Salaün
2020-11-11 21:34 ` [PATCH v1 2/9] landlock: Cosmetic fixes for filesystem management Mickaël Salaün
2020-11-20  1:37   ` James Morris
2020-11-11 21:34 ` Mickaël Salaün [this message]
2020-11-11 21:34 ` [PATCH v1 4/9] landlock: Always intersect access rights Mickaël Salaün
2020-11-11 21:34 ` [PATCH v1 5/9] landlock: Add extra checks when inserting a rule Mickaël Salaün
2020-11-11 21:34 ` [PATCH v1 6/9] selftests/landlock: Extend layout1.inherit_superset Mickaël Salaün
2020-11-11 21:34 ` [PATCH v1 7/9] landlock: Clean up get_ruleset_from_fd() Mickaël Salaün
2020-11-11 21:34 ` [PATCH v1 8/9] landlock: Add help to enable Landlock as a stacked LSM Mickaël Salaün
2020-11-11 21:34 ` [PATCH v1 9/9] landlock: Extend documentation about limitations Mickaël Salaün
2020-11-12  4:59 ` [PATCH v1 0/9] Landlock fixes James Morris

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20201111213442.434639-4-mic@digikod.net \
    --to=mic@digikod.net \
    --cc=jannh@google.com \
    --cc=jmorris@namei.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=linux-security-module@vger.kernel.org \
    --cc=serge@hallyn.com \
    --cc=shuah@kernel.org \
    --cc=vincent.dagonneau@ssi.gouv.fr \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).