From patchwork Sat Mar 8 18:43:57 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007672 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-42a9.mail.infomaniak.ch (smtp-42a9.mail.infomaniak.ch [84.16.66.169]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id B676F15E5DC for ; Sat, 8 Mar 2025 18:44:42 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=84.16.66.169 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459486; cv=none; b=IAOFLFLKGVCWsm2pZ4ttk6tmf9RfQjVoUz7Nm67SYncrvPm2KJsE8CmNnD5UXy/Qx2ShTBOwzRdJW4LVjPQ1VlrD9KNggJU6f2LTKtZOJvgZczSMWp/McrOZ5GONxIStYEfT19u3GV99A5T5DH2JeSnO2U1dE2BJmV96cBJLYIg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459486; c=relaxed/simple; bh=mBOPs9rzB+SeHsLDPQt3VFlTdlYzvqYWGa2aaMV/u6Q=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=jDpVdTKuEgKC1a/21X4JpfO5xg5YrQPKMSdKfVzbSHh4MYiTGa77hHkkrAUTg5yMP/Ff1HyI06tV/42AA7iqI6UHQQ7s2AsoiDaPbUxLe1d969330pixUjGQE+z4OAsE19/G6YqvVRKgfydBHkqwSToZPVLC8LlBqDn5XcRScik= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=F2tABzZu; arc=none smtp.client-ip=84.16.66.169 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="F2tABzZu" Received: from smtp-4-0000.mail.infomaniak.ch (unknown [IPv6:2001:1600:7:10:40ca:feff:fe05:0]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9BsH0nzrzStD; Sat, 8 Mar 2025 19:44:35 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459474; bh=DdXEvEUEuCzLG8T1qTLKV770RvuwQMh1ndZL3ZZ5Fog=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=F2tABzZu0/Rfs7Wqlj4a0GCX2qkywtqTgBoByqViaPBz9hy72uCWH8PmALdcklc5F jsYJvQnPdhFbT8W2WGY2xJ9/7v6rMTXcZqPO8/lqXLYj4l628MUN1NEBsiwuSTkrjU RYAiuJt+EnHtm0WZ4GAjTuxyCCQ8zXGAkHmztl54= Received: from unknown by smtp-4-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsG2gYSzH7H; Sat, 8 Mar 2025 19:44:34 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 01/26] lsm: Add audit_log_lsm_data() helper Date: Sat, 8 Mar 2025 19:43:57 +0100 Message-ID: <20250308184422.2159360-2-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Extract code from dump_common_audit_data() into the audit_log_lsm_data() helper. This helps reuse common LSM audit data while not abusing AUDIT_AVC records because of the common_lsm_audit() helper. Cc: Casey Schaufler Cc: James Morris Cc: Serge E. Hallyn Acked-by: Paul Moore Depends-on: 7ccbe076d987 ("lsm: Only build lsm_audit.c if CONFIG_SECURITY and CONFIG_AUDIT are set") Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-2-mic@digikod.net --- Changes since v4: - Add Depends-on tag. Changes since v3: - Rebase on top of the v6.13's get_task_comm() fix. - Add Acked-by Paul. Changes since v1: - Fix commit message (spotted by Paul). - Constify dump_common_audit_data()'s and audit_log_lsm_data()'s "a" argument. - Fix build without CONFIG_NET: see previous patch. --- include/linux/lsm_audit.h | 8 ++++++++ security/lsm_audit.c | 27 ++++++++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/include/linux/lsm_audit.h b/include/linux/lsm_audit.h index e13d2f947b51..bddd694f7c4c 100644 --- a/include/linux/lsm_audit.h +++ b/include/linux/lsm_audit.h @@ -132,6 +132,9 @@ void common_lsm_audit(struct common_audit_data *a, void (*pre_audit)(struct audit_buffer *, void *), void (*post_audit)(struct audit_buffer *, void *)); +void audit_log_lsm_data(struct audit_buffer *ab, + const struct common_audit_data *a); + #else /* CONFIG_AUDIT */ static inline void common_lsm_audit(struct common_audit_data *a, @@ -140,6 +143,11 @@ static inline void common_lsm_audit(struct common_audit_data *a, { } +static inline void audit_log_lsm_data(struct audit_buffer *ab, + const struct common_audit_data *a) +{ +} + #endif /* CONFIG_AUDIT */ #endif diff --git a/security/lsm_audit.c b/security/lsm_audit.c index 52db886dbba8..a61c7ebdb6a7 100644 --- a/security/lsm_audit.c +++ b/security/lsm_audit.c @@ -189,16 +189,13 @@ static inline void print_ipv4_addr(struct audit_buffer *ab, __be32 addr, } /** - * dump_common_audit_data - helper to dump common audit data + * audit_log_lsm_data - helper to log common LSM audit data * @ab : the audit buffer * @a : common audit data - * */ -static void dump_common_audit_data(struct audit_buffer *ab, - struct common_audit_data *a) +void audit_log_lsm_data(struct audit_buffer *ab, + const struct common_audit_data *a) { - char comm[sizeof(current->comm)]; - /* * To keep stack sizes in check force programmers to notice if they * start making this union too large! See struct lsm_network_audit @@ -206,9 +203,6 @@ static void dump_common_audit_data(struct audit_buffer *ab, */ BUILD_BUG_ON(sizeof(a->u) > sizeof(void *)*2); - audit_log_format(ab, " pid=%d comm=", task_tgid_nr(current)); - audit_log_untrustedstring(ab, get_task_comm(comm, current)); - switch (a->type) { case LSM_AUDIT_DATA_NONE: return; @@ -431,6 +425,21 @@ static void dump_common_audit_data(struct audit_buffer *ab, } /* switch (a->type) */ } +/** + * dump_common_audit_data - helper to dump common audit data + * @ab : the audit buffer + * @a : common audit data + */ +static void dump_common_audit_data(struct audit_buffer *ab, + const struct common_audit_data *a) +{ + char comm[sizeof(current->comm)]; + + audit_log_format(ab, " pid=%d comm=", task_tgid_nr(current)); + audit_log_untrustedstring(ab, get_task_comm(comm, current)); + audit_log_lsm_data(ab, a); +} + /** * common_lsm_audit - generic LSM auditing function * @a: auxiliary audit data From patchwork Sat Mar 8 18:43:58 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007673 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-190a.mail.infomaniak.ch (smtp-190a.mail.infomaniak.ch [185.125.25.10]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id D01791EB5EB for ; Sat, 8 Mar 2025 18:44:43 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.125.25.10 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459487; cv=none; b=feCfEPvnhKErp2M9f3YcZr2whfOUt7e9HREYhDpB8NPEwAgQcGoRsATFhdx4EQAgvYRi56UKzwC7m0fQE34yLNVprImMznZ3nKB9bunXFuV9OFAdODYZ7JVP9ICqTZuijnbnpRrMLnZi8mBkZg+V/tiAemlsCuMlUVSbz6nqwyM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459487; c=relaxed/simple; bh=OrWU8l8L3Y2zaWDYRB2lVaBp8aGMEaqw0DoNCB5Ie4U=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=U0bAaFjM+y2HqZH0r2dbb5Gxy5nQs/XzPxd67OlTgp0Q5HZDfGhc4TrXd83qNtXUpfOsxDGbuYLuPFMooa6n8/5WIlyy38CfIfeQKBcaI7cic/E8ovzvp0YOkUvuWLRaoquNKCarVAKXiNXtUpk3n2awDRRv0QfM/AmEStNnVwU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=eaUbBdU1; arc=none smtp.client-ip=185.125.25.10 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="eaUbBdU1" Received: from smtp-3-0000.mail.infomaniak.ch (smtp-3-0000.mail.infomaniak.ch [10.4.36.107]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9BsJ3lRZzMPf; Sat, 8 Mar 2025 19:44:36 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459476; bh=Yc09BG4KYMcxWte01JUQliuw7zJF2EwT3ZONkyCTbY8=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=eaUbBdU1iqp6i/l7kKtNfYm12fycugRq6tFKPA9pHxDTNNhyDLf7OTOSan/XDP+Jb b0bYgiEptvtJmIyTGs/1QUIl1s9I/hnLfctZopOHwai3l7nOFQN8b+K8ctSvskE5Y0 kkpPzPvmT6p4X5ThxIjVkHbehg5s19db1ljY16gQ= Received: from unknown by smtp-3-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsH5hSyzr5v; Sat, 8 Mar 2025 19:44:35 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 02/26] landlock: Add unique ID generator Date: Sat, 8 Mar 2025 19:43:58 +0100 Message-ID: <20250308184422.2159360-3-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Landlock IDs can be generated to uniquely identify Landlock objects. For now, only Landlock domains get an ID at creation time. These IDs map to immutable domain hierarchies. Landlock IDs have important properties: - They are unique during the lifetime of the running system thanks to the 64-bit values: at worse, 2^60 - 2*2^32 useful IDs. - They are always greater than 2^32 and must then be stored in 64-bit integer types. - The initial ID (at boot time) is randomly picked between 2^32 and 2^33, which limits collisions in logs across different boots. - IDs are sequential, which enables users to order them. - IDs may not be consecutive but increase with a random 2^4 step, which limits side channels. Such IDs can be exposed to unprivileged processes, even if it is not the case with this audit patch series. The domain IDs will be useful for user space to identify sandboxes and get their properties. These Landlock IDs are more secure that other absolute kernel IDs such as pipe's inodes which rely on a shared global counter. For checkpoint/restore features (i.e. CRIU), we could easily implement a privileged interface (e.g. sysfs) to set the next ID counter. IDR/IDA are not used because we only need a bijection from Landlock objects to Landlock IDs, and we must not recycle IDs. This enables us to identify all Landlock objects during the lifetime of the system (e.g. in logs), but not to access an object from an ID nor know if an ID is assigned. Using a counter is simpler, it scales (i.e. avoids growing memory footprint), and it does not require locking. We'll use proper file descriptors (with IDs used as inode numbers) to access Landlock objects. Cc: Günther Noack Cc: Paul Moore Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-3-mic@digikod.net --- Changes since v5: - Add KUnit error message, suggested by Günther. Changes since v3: - Rename landlock_get_id_range() helper to reflect the "range" of IDs. - Add docstring for landlock_get_id_range(). Changes since v2: - Extend commit message. - Rename global_counter to next_id. - Fix KUnit's test __init types, spotted by kernel test robot. Changes since v1: - New patch. --- security/landlock/.kunitconfig | 2 + security/landlock/Makefile | 2 + security/landlock/id.c | 251 +++++++++++++++++++ security/landlock/id.h | 25 ++ security/landlock/setup.c | 2 + tools/testing/kunit/configs/all_tests.config | 2 + 6 files changed, 284 insertions(+) create mode 100644 security/landlock/id.c create mode 100644 security/landlock/id.h diff --git a/security/landlock/.kunitconfig b/security/landlock/.kunitconfig index 03e119466604..f9423f01ac5b 100644 --- a/security/landlock/.kunitconfig +++ b/security/landlock/.kunitconfig @@ -1,4 +1,6 @@ +CONFIG_AUDIT=y CONFIG_KUNIT=y +CONFIG_NET=y CONFIG_SECURITY=y CONFIG_SECURITY_LANDLOCK=y CONFIG_SECURITY_LANDLOCK_KUNIT_TEST=y diff --git a/security/landlock/Makefile b/security/landlock/Makefile index b4538b7cf7d2..e1777abbc413 100644 --- a/security/landlock/Makefile +++ b/security/landlock/Makefile @@ -4,3 +4,5 @@ landlock-y := setup.o syscalls.o object.o ruleset.o \ cred.o task.o fs.o landlock-$(CONFIG_INET) += net.o + +landlock-$(CONFIG_AUDIT) += id.o diff --git a/security/landlock/id.c b/security/landlock/id.c new file mode 100644 index 000000000000..3e2bd6661822 --- /dev/null +++ b/security/landlock/id.c @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock LSM - Unique identification number generator + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#include +#include +#include +#include + +#include "common.h" +#include "id.h" + +#define COUNTER_PRE_INIT 0 + +static atomic64_t next_id = ATOMIC64_INIT(COUNTER_PRE_INIT); + +static void __init init_id(atomic64_t *const counter, const u32 random_32bits) +{ + u64 init; + + /* + * Ensures sure 64-bit values are always used by user space (or may + * fail with -EOVERFLOW), and makes this testable. + */ + init = 1ULL << 32; + + /* + * Makes a large (2^32) boot-time value to limit ID collision in logs + * from different boots, and to limit info leak about the number of + * initially (relative to the reader) created elements (e.g. domains). + */ + init += random_32bits; + + /* Sets first or ignores. This will be the first ID. */ + atomic64_cmpxchg(counter, COUNTER_PRE_INIT, init); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void __init test_init_min(struct kunit *const test) +{ + atomic64_t counter = ATOMIC64_INIT(COUNTER_PRE_INIT); + + init_id(&counter, 0); + KUNIT_EXPECT_EQ(test, atomic64_read(&counter), 1ULL + U32_MAX); +} + +static void __init test_init_max(struct kunit *const test) +{ + atomic64_t counter = ATOMIC64_INIT(COUNTER_PRE_INIT); + + init_id(&counter, ~0); + KUNIT_EXPECT_EQ(test, atomic64_read(&counter), 1 + (2ULL * U32_MAX)); +} + +static void __init test_init_once(struct kunit *const test) +{ + const u64 first_init = 1ULL + U32_MAX; + atomic64_t counter = ATOMIC64_INIT(COUNTER_PRE_INIT); + + init_id(&counter, 0); + KUNIT_EXPECT_EQ(test, atomic64_read(&counter), first_init); + + init_id(&counter, ~0); + KUNIT_EXPECT_EQ_MSG( + test, atomic64_read(&counter), first_init, + "Should still have the same value after the subsequent init_id()"); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +void __init landlock_init_id(void) +{ + return init_id(&next_id, get_random_u32()); +} + +/* + * It's not worth it to try to hide the monotonic counter because it can still + * be inferred (with N counter ranges), and if we are allowed to read the inode + * number we should also be allowed to read the time creation anyway, and it + * can be handy to store and sort domain IDs for user space. + * + * Returns the value of next_id and increment it to let some space for the next + * one. + */ +static u64 get_id_range(size_t number_of_ids, atomic64_t *const counter, + u8 random_4bits) +{ + u64 id, step; + + /* + * We should return at least 1 ID, and we may need a set of consecutive + * ones (e.g. to generate a set of inodes). + */ + if (WARN_ON_ONCE(number_of_ids <= 0)) + number_of_ids = 1; + + /* + * Blurs the next ID guess with 1/16 ratio. We get 2^(64 - 4) - + * (2 * 2^32), so a bit less than 2^60 available IDs, which should be + * much more than enough considering the number of CPU cycles required + * to get a new ID (e.g. a full landlock_restrict_self() call), and the + * cost of draining all available IDs during the system's uptime. + */ + random_4bits = random_4bits % (1 << 4); + step = number_of_ids + random_4bits; + + /* It is safe to cast a signed atomic to an unsigned value. */ + id = atomic64_fetch_add(step, counter); + + /* Warns if landlock_init_id() was not called. */ + WARN_ON_ONCE(id == COUNTER_PRE_INIT); + return id; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_range1_rand0(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(1, &counter, 0), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 1); +} + +static void test_range1_rand1(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(1, &counter, 1), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 2); +} + +static void test_range1_rand16(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(1, &counter, 16), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 1); +} + +static void test_range2_rand0(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 0), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 2); +} + +static void test_range2_rand1(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 1), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 3); +} + +static void test_range2_rand2(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 2), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 4); +} + +static void test_range2_rand16(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 16), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 2); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +/** + * landlock_get_id_range - Get a range of unique IDs + * + * @number_of_ids: Number of IDs to hold. Must be greater than one. + * + * Returns: The first ID in the range. + */ +u64 landlock_get_id_range(size_t number_of_ids) +{ + return get_id_range(number_of_ids, &next_id, get_random_u8()); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static struct kunit_case __refdata test_cases[] = { + /* clang-format off */ + KUNIT_CASE(test_init_min), + KUNIT_CASE(test_init_max), + KUNIT_CASE(test_init_once), + KUNIT_CASE(test_range1_rand0), + KUNIT_CASE(test_range1_rand1), + KUNIT_CASE(test_range1_rand16), + KUNIT_CASE(test_range2_rand0), + KUNIT_CASE(test_range2_rand1), + KUNIT_CASE(test_range2_rand2), + KUNIT_CASE(test_range2_rand16), + {} + /* clang-format on */ +}; + +static struct kunit_suite test_suite = { + .name = "landlock_id", + .test_cases = test_cases, +}; + +kunit_test_init_section_suite(test_suite); + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ diff --git a/security/landlock/id.h b/security/landlock/id.h new file mode 100644 index 000000000000..99f596123c19 --- /dev/null +++ b/security/landlock/id.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Unique identification number generator + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#ifndef _SECURITY_LANDLOCK_ID_H +#define _SECURITY_LANDLOCK_ID_H + +#ifdef CONFIG_AUDIT + +void __init landlock_init_id(void); + +u64 landlock_get_id_range(size_t number_of_ids); + +#else /* CONFIG_AUDIT */ + +static inline void __init landlock_init_id(void) +{ +} + +#endif /* CONFIG_AUDIT */ + +#endif /* _SECURITY_LANDLOCK_ID_H */ diff --git a/security/landlock/setup.c b/security/landlock/setup.c index 28519a45b11f..d297083efcb1 100644 --- a/security/landlock/setup.c +++ b/security/landlock/setup.c @@ -13,6 +13,7 @@ #include "common.h" #include "cred.h" #include "fs.h" +#include "id.h" #include "net.h" #include "setup.h" #include "task.h" @@ -33,6 +34,7 @@ const struct lsm_id landlock_lsmid = { static int __init landlock_init(void) { + landlock_init_id(); landlock_add_cred_hooks(); landlock_add_task_hooks(); landlock_add_fs_hooks(); diff --git a/tools/testing/kunit/configs/all_tests.config b/tools/testing/kunit/configs/all_tests.config index b0049be00c70..cdd9782f9646 100644 --- a/tools/testing/kunit/configs/all_tests.config +++ b/tools/testing/kunit/configs/all_tests.config @@ -41,6 +41,8 @@ CONFIG_DAMON_PADDR=y CONFIG_REGMAP_BUILD=y +CONFIG_AUDIT=y + CONFIG_SECURITY=y CONFIG_SECURITY_APPARMOR=y CONFIG_SECURITY_LANDLOCK=y From patchwork Sat Mar 8 18:43:59 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007674 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-1908.mail.infomaniak.ch (smtp-1908.mail.infomaniak.ch [185.125.25.8]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id B0F6C1FC7D9 for ; Sat, 8 Mar 2025 18:44:45 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.125.25.8 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459490; cv=none; b=kLZ8IVg/FOHsEpLcAAQ+r7/Na/bdTvjy6AA6+85OQSxl4rCe8NXIq9pl5tUXIrZp20gQrg2OQeYzTOCGs4snw0jZnI5aHYxDAwXFHa6X9jMu6bbhviuQtgpCxuBIEeMEEZTLeUTF3i8MHLX90B0R9vDjBWdsATx6gjo4pdxkqq0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459490; c=relaxed/simple; bh=/DIW+o9MtyBqi9NSDKP1dznkr/BGT7d3uJ58sf94IQ0=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=gF46IOpkjSBICJuLMvXS1RNTW24jOTQQX2AIqKTZzmZQXGH9cgQgskrPPn8kPvQ+Pk/05TWZZergZdeshAxL7fuSV38BXOZK6ZJzTM5S8cZWEcSNorT0/dPx4RThC0dxc69Us+4YuIMQ5VIjf8BxMUr+IVGaUPSxAd17cDdvTog= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=PsGar88P; arc=none smtp.client-ip=185.125.25.8 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="PsGar88P" Received: from smtp-3-0000.mail.infomaniak.ch (smtp-3-0000.mail.infomaniak.ch [10.4.36.107]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9BsK5h1nzQ5y; Sat, 8 Mar 2025 19:44:37 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459477; bh=KEWxcLsnhjmJc3O6glXK+S8YI69l+/vlv/Gj9V16MQQ=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=PsGar88PKisKUNdvF7leuI3ysvAVX3CKKesUBVxxuEscR6Mp5SEKTDs9muh6y7jp+ mJAMIC+VTM+mIOo8chP/ZRaVlfx/85SGrG/4hD5CI+B2P98oUtPqVfJOEUaRpQrkwJ 09CbHWna27RY1+QVEfgd49Jozk0DpFphfgIT49YU= Received: from unknown by smtp-3-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsK0wJXzn94; Sat, 8 Mar 2025 19:44:37 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 03/26] landlock: Move domain hierarchy management Date: Sat, 8 Mar 2025 19:43:59 +0100 Message-ID: <20250308184422.2159360-4-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Create a new domain.h file containing the struct landlock_hierarchy definition and helpers. This type will grow with audit support. This also prepares for a new domain type. Cc: Günther Noack Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-4-mic@digikod.net --- Changes since v4: - Revert v3 changes because of the new audit rule patch removal. Changes since v3: - Export landlock_get_hierarchy() and landlock_put_hierarchy(). - Clean up Makefile entries. Changes since v1: - New patch. --- security/landlock/domain.h | 48 +++++++++++++++++++++++++++++++++++++ security/landlock/ruleset.c | 21 +++------------- security/landlock/ruleset.h | 17 +------------ security/landlock/task.c | 1 + 4 files changed, 53 insertions(+), 34 deletions(-) create mode 100644 security/landlock/domain.h diff --git a/security/landlock/domain.h b/security/landlock/domain.h new file mode 100644 index 000000000000..015d61fd81ec --- /dev/null +++ b/security/landlock/domain.h @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Domain management + * + * Copyright © 2016-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ + +#ifndef _SECURITY_LANDLOCK_DOMAIN_H +#define _SECURITY_LANDLOCK_DOMAIN_H + +#include +#include + +/** + * struct landlock_hierarchy - Node in a domain hierarchy + */ +struct landlock_hierarchy { + /** + * @parent: Pointer to the parent node, or NULL if it is a root + * Landlock domain. + */ + struct landlock_hierarchy *parent; + /** + * @usage: Number of potential children domains plus their parent + * domain. + */ + refcount_t usage; +}; + +static inline void +landlock_get_hierarchy(struct landlock_hierarchy *const hierarchy) +{ + if (hierarchy) + refcount_inc(&hierarchy->usage); +} + +static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy) +{ + while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) { + const struct landlock_hierarchy *const freeme = hierarchy; + + hierarchy = hierarchy->parent; + kfree(freeme); + } +} + +#endif /* _SECURITY_LANDLOCK_DOMAIN_H */ diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index bff4e40a3093..adb7f87828df 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -23,6 +23,7 @@ #include #include "access.h" +#include "domain.h" #include "limits.h" #include "object.h" #include "ruleset.h" @@ -307,22 +308,6 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset, return insert_rule(ruleset, id, &layers, ARRAY_SIZE(layers)); } -static void get_hierarchy(struct landlock_hierarchy *const hierarchy) -{ - if (hierarchy) - refcount_inc(&hierarchy->usage); -} - -static void put_hierarchy(struct landlock_hierarchy *hierarchy) -{ - while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) { - const struct landlock_hierarchy *const freeme = hierarchy; - - hierarchy = hierarchy->parent; - kfree(freeme); - } -} - static int merge_tree(struct landlock_ruleset *const dst, struct landlock_ruleset *const src, const enum landlock_key_type key_type) @@ -477,7 +462,7 @@ static int inherit_ruleset(struct landlock_ruleset *const parent, err = -EINVAL; goto out_unlock; } - get_hierarchy(parent->hierarchy); + landlock_get_hierarchy(parent->hierarchy); child->hierarchy->parent = parent->hierarchy; out_unlock: @@ -501,7 +486,7 @@ static void free_ruleset(struct landlock_ruleset *const ruleset) free_rule(freeme, LANDLOCK_KEY_NET_PORT); #endif /* IS_ENABLED(CONFIG_INET) */ - put_hierarchy(ruleset->hierarchy); + landlock_put_hierarchy(ruleset->hierarchy); kfree(ruleset); } diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h index 52f4f0af6ab0..bbb5996545d2 100644 --- a/security/landlock/ruleset.h +++ b/security/landlock/ruleset.h @@ -17,6 +17,7 @@ #include #include "access.h" +#include "domain.h" #include "limits.h" #include "object.h" @@ -108,22 +109,6 @@ struct landlock_rule { struct landlock_layer layers[] __counted_by(num_layers); }; -/** - * struct landlock_hierarchy - Node in a ruleset hierarchy - */ -struct landlock_hierarchy { - /** - * @parent: Pointer to the parent node, or NULL if it is a root - * Landlock domain. - */ - struct landlock_hierarchy *parent; - /** - * @usage: Number of potential children domains plus their parent - * domain. - */ - refcount_t usage; -}; - /** * struct landlock_ruleset - Landlock ruleset * diff --git a/security/landlock/task.c b/security/landlock/task.c index dc7dab78392e..98894ad1abc7 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -18,6 +18,7 @@ #include "common.h" #include "cred.h" +#include "domain.h" #include "fs.h" #include "ruleset.h" #include "setup.h" From patchwork Sat Mar 8 18:44:00 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007713 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-8fa8.mail.infomaniak.ch (smtp-8fa8.mail.infomaniak.ch [83.166.143.168]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 029D11A7AF7 for ; Sat, 8 Mar 2025 19:00:12 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=83.166.143.168 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741460427; cv=none; b=KWUXM04YqH/neHAuD1vKL2QJWmRfxYLu+BM/xBAdfSQBgWO6VQh4KY/KN/Kj2FgKu2cu29Hp1lzaW31nNpm0MMKOh5KuLA6WcS+2db56gvZH4Txfc5Yr7e5jig9w3w9bDu/Zwlb/wLDn5UV0yBRUYY5K6Ejl5nDkfDV83uwttco= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741460427; c=relaxed/simple; bh=AmtaGvKQQlXjo3JdTiWFG/rOYA6Xnap3wIdH9lIvMFU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=umuOp/F1UNow/c2whTjhwqp1N2+LSFaQS7/PqIH1E1VDqtrE6xFOmhOsbyy1+V+8vsGQM/DOiGJpdl2jlEI5lqKkEsSjFDOqBJficEEWMoWFxsNj8JXoIPMY1hZumErBeTwPzNqdE14CxAXLLREjhBv8LSPMwTDFHYgAjG7aZV8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=PYz3XabL; arc=none smtp.client-ip=83.166.143.168 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="PYz3XabL" Received: from smtp-3-0001.mail.infomaniak.ch (unknown [IPv6:2001:1600:4:17::246c]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9BsM12pgzRSy; Sat, 8 Mar 2025 19:44:39 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459479; bh=TwH8+8Lj6sHCNuDChiWjCXEj20CBT3AkbxLELBQclZY=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=PYz3XabLHO0kwUd4p8LitPg1gLDmLSEJgPCqHI8WnabubJnnDRbpAJGez9J2sOpdf FuHSnLdfHDANMBUfbf4doH+DR2Rbe74WrfjjH7uCDdWXaBREE/PqwOc0xM36jxTnfX jTbnxJQHyNVbl8LlYMEwJXqKP42RRnYaH9RfXrcE= Received: from unknown by smtp-3-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsL3fNzzMX; Sat, 8 Mar 2025 19:44:38 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 04/26] landlock: Prepare to use credential instead of domain for filesystem Date: Sat, 8 Mar 2025 19:44:00 +0100 Message-ID: <20250308184422.2159360-5-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha This cosmetic change is needed for audit support, specifically to be able to filter according to cross-execution boundaries. Add landlock_get_applicable_subject(), mainly a copy of landlock_get_applicable_domain(), which will fully replace it in a following commit. Optimize current_check_access_path() to only handle the access request. Partially replace get_current_fs_domain() with explicit calls to landlock_get_applicable_subject(). The remaining ones will follow with more changes. Remove explicit domain->num_layers check which is now part of the landlock_get_applicable_subject() call. Cc: Günther Noack Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-5-mic@digikod.net --- Changes since v4: - New patch to avoid duplicated computation of the layer level that denied an access. --- security/landlock/cred.h | 50 +++++++++++++++++++++++++++++++ security/landlock/fs.c | 65 +++++++++++++++++++++++----------------- 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/security/landlock/cred.h b/security/landlock/cred.h index bf755459838a..fdbbaf66d151 100644 --- a/security/landlock/cred.h +++ b/security/landlock/cred.h @@ -13,6 +13,7 @@ #include #include +#include "access.h" #include "ruleset.h" #include "setup.h" @@ -53,6 +54,55 @@ static inline bool landlocked(const struct task_struct *const task) return has_dom; } +/** + * landlock_get_applicable_subject - Return the subject's Landlock credential + * if its enforced domain applies to (i.e. + * handles) at least one of the access rights + * specified in @masks + * + * @cred: credential + * @masks: access masks + * @handle_layer: returned youngest layer handling a subset of @masks. Not set + * if the function returns NULL. + * + * Returns: landlock_cred(@cred) if any access rights specified in @masks is + * handled, or NULL otherwise. + */ +static inline const struct landlock_cred_security * +landlock_get_applicable_subject(const struct cred *const cred, + const struct access_masks masks, + size_t *const handle_layer) +{ + const union access_masks_all masks_all = { + .masks = masks, + }; + const struct landlock_ruleset *domain; + ssize_t layer_level; + + if (!cred) + return NULL; + + domain = landlock_cred(cred)->domain; + if (!domain) + return NULL; + + for (layer_level = domain->num_layers - 1; layer_level >= 0; + layer_level--) { + union access_masks_all layer = { + .masks = domain->access_masks[layer_level], + }; + + if (layer.all & masks_all.all) { + if (handle_layer) + *handle_layer = layer_level; + + return landlock_cred(cred); + } + } + + return NULL; +} + __init void landlock_add_cred_hooks(void); #endif /* _SECURITY_LANDLOCK_CRED_H */ diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 71b9dc331aae..d5b153d29fcb 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -771,11 +771,14 @@ static bool is_access_to_paths_allowed( if (!access_request_parent1 && !access_request_parent2) return true; - if (WARN_ON_ONCE(!domain || !path)) + + if (WARN_ON_ONCE(!path)) return true; + if (is_nouser_or_private(path->dentry)) return true; - if (WARN_ON_ONCE(domain->num_layers < 1 || !layer_masks_parent1)) + + if (WARN_ON_ONCE(!layer_masks_parent1)) return false; allowed_parent1 = is_layer_masks_allowed(layer_masks_parent1); @@ -926,16 +929,21 @@ static bool is_access_to_paths_allowed( static int current_check_access_path(const struct path *const path, access_mask_t access_request) { - const struct landlock_ruleset *const dom = get_current_fs_domain(); + const struct access_masks masks = { + .fs = access_request, + }; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), masks, NULL); layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {}; - if (!dom) + if (!subject) return 0; - access_request = landlock_init_layer_masks( - dom, access_request, &layer_masks, LANDLOCK_KEY_INODE); - if (is_access_to_paths_allowed(dom, path, access_request, &layer_masks, - NULL, 0, NULL, NULL)) + access_request = landlock_init_layer_masks(subject->domain, + access_request, &layer_masks, + LANDLOCK_KEY_INODE); + if (is_access_to_paths_allowed(subject->domain, path, access_request, + &layer_masks, NULL, 0, NULL, NULL)) return 0; return -EACCES; @@ -1098,7 +1106,8 @@ static int current_check_refer_path(struct dentry *const old_dentry, struct dentry *const new_dentry, const bool removable, const bool exchange) { - const struct landlock_ruleset *const dom = get_current_fs_domain(); + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, NULL); bool allow_parent1, allow_parent2; access_mask_t access_request_parent1, access_request_parent2; struct path mnt_dir; @@ -1106,10 +1115,9 @@ static int current_check_refer_path(struct dentry *const old_dentry, layer_mask_t layer_masks_parent1[LANDLOCK_NUM_ACCESS_FS] = {}, layer_masks_parent2[LANDLOCK_NUM_ACCESS_FS] = {}; - if (!dom) + if (!subject) return 0; - if (WARN_ON_ONCE(dom->num_layers < 1)) - return -EACCES; + if (unlikely(d_is_negative(old_dentry))) return -ENOENT; if (exchange) { @@ -1134,10 +1142,11 @@ static int current_check_refer_path(struct dentry *const old_dentry, * for same-directory referer (i.e. no reparenting). */ access_request_parent1 = landlock_init_layer_masks( - dom, access_request_parent1 | access_request_parent2, + subject->domain, + access_request_parent1 | access_request_parent2, &layer_masks_parent1, LANDLOCK_KEY_INODE); if (is_access_to_paths_allowed( - dom, new_dir, access_request_parent1, + subject->domain, new_dir, access_request_parent1, &layer_masks_parent1, NULL, 0, NULL, NULL)) return 0; return -EACCES; @@ -1160,10 +1169,12 @@ static int current_check_refer_path(struct dentry *const old_dentry, old_dentry->d_parent; /* new_dir->dentry is equal to new_dentry->d_parent */ - allow_parent1 = collect_domain_accesses(dom, mnt_dir.dentry, old_parent, + allow_parent1 = collect_domain_accesses(subject->domain, mnt_dir.dentry, + old_parent, &layer_masks_parent1); - allow_parent2 = collect_domain_accesses( - dom, mnt_dir.dentry, new_dir->dentry, &layer_masks_parent2); + allow_parent2 = collect_domain_accesses(subject->domain, mnt_dir.dentry, + new_dir->dentry, + &layer_masks_parent2); if (allow_parent1 && allow_parent2) return 0; @@ -1175,9 +1186,9 @@ static int current_check_refer_path(struct dentry *const old_dentry, * destination parent access rights. */ if (is_access_to_paths_allowed( - dom, &mnt_dir, access_request_parent1, &layer_masks_parent1, - old_dentry, access_request_parent2, &layer_masks_parent2, - exchange ? new_dentry : NULL)) + subject->domain, &mnt_dir, access_request_parent1, + &layer_masks_parent1, old_dentry, access_request_parent2, + &layer_masks_parent2, exchange ? new_dentry : NULL)) return 0; /* @@ -1504,11 +1515,10 @@ static int hook_file_open(struct file *const file) layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {}; access_mask_t open_access_request, full_access_request, allowed_access, optional_access; - const struct landlock_ruleset *const dom = - landlock_get_applicable_domain( - landlock_cred(file->f_cred)->domain, any_fs); + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(file->f_cred, any_fs, NULL); - if (!dom) + if (!subject) return 0; /* @@ -1529,9 +1539,10 @@ static int hook_file_open(struct file *const file) full_access_request = open_access_request | optional_access; if (is_access_to_paths_allowed( - dom, &file->f_path, - landlock_init_layer_masks(dom, full_access_request, - &layer_masks, LANDLOCK_KEY_INODE), + subject->domain, &file->f_path, + landlock_init_layer_masks(subject->domain, + full_access_request, &layer_masks, + LANDLOCK_KEY_INODE), &layer_masks, NULL, 0, NULL, NULL)) { allowed_access = full_access_request; } else { From patchwork Sat Mar 8 18:44:01 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007701 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-8fad.mail.infomaniak.ch (smtp-8fad.mail.infomaniak.ch [83.166.143.173]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A074B18DB33 for ; Sat, 8 Mar 2025 18:50:11 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=83.166.143.173 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459814; cv=none; b=ooDpRhIil1a/wxbwI8DSqN0q3oD7cogD4+n+uUyzjMDhqTR1fk5a8EzN1EZjz+DdK+RNl52qMSt1DxORAmhXzDXFWC9xox1Jn3KHQOOJOPiW+r1qyXAmZg/GqWrDz1cujm8b9yS3CGvJJgFH9tl7xx/5W89a73Ci06fpQh5dQV4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459814; c=relaxed/simple; bh=MXaLO2V6ZjDmrW7A119TF5SuxXeQ2/ryPO4zhKxu70o=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=JZ9QmViZzFz4BljBzL3YXBYKmsyl0JuiG4tExIbqDyL8oSpbwURpZCSMRMJB2BNoIcVHzdlZjj2D0kkyTRbhQlxhTxQJxWKG6fu0rrchAIKB6TaSCqMJxMX16QqZwROsI9ycRDMcBnjS1lt/w+9vahryEAvVcK7q6uWspUAXUJo= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=b0ge4cY6; arc=none smtp.client-ip=83.166.143.173 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="b0ge4cY6" Received: from smtp-4-0001.mail.infomaniak.ch (unknown [IPv6:2001:1600:7:10:40ca:feff:fe05:1]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9BsN55G7zNql; Sat, 8 Mar 2025 19:44:40 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459480; bh=61Lm58nZn5dPCvFx6teXyXEqJvov75x4EeQ4yxThK7Y=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=b0ge4cY6XNXkWXmPBw59WUF26NKXcf08yreHSar6bee8tiZH/P9Zw3aiATkxZbuGf yxaVII3gCC5Kj8+Y4oqz5ESzCzAxin90HA1Trk1NO9N8m7vobmSQEfoQQqO9x5n0h9 /fkUFFEYXXNdSUQhb6CkWb56zNa875KVNKEym9pU= Received: from unknown by smtp-4-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsM6GVnzhtB; Sat, 8 Mar 2025 19:44:39 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 05/26] landlock: Prepare to use credential instead of domain for network Date: Sat, 8 Mar 2025 19:44:01 +0100 Message-ID: <20250308184422.2159360-6-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha This cosmetic change that is needed for audit support, specifically to be able to filter according to cross-execution boundaries. Optimize current_check_access_socket() to only handle the access request. Remove explicit domain->num_layers check which is now part of the landlock_get_applicable_subject() call. Cc: Günther Noack Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-6-mic@digikod.net --- Changes since v4: - New patch. --- security/landlock/net.c | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/security/landlock/net.c b/security/landlock/net.c index 104b6c01fe50..6fb3e60bc5ff 100644 --- a/security/landlock/net.c +++ b/security/landlock/net.c @@ -39,10 +39,6 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset, return err; } -static const struct access_masks any_net = { - .net = ~0, -}; - static int current_check_access_socket(struct socket *const sock, struct sockaddr *const address, const int addrlen, @@ -54,14 +50,14 @@ static int current_check_access_socket(struct socket *const sock, struct landlock_id id = { .type = LANDLOCK_KEY_NET_PORT, }; - const struct landlock_ruleset *const dom = - landlock_get_applicable_domain(landlock_get_current_domain(), - any_net); + const struct access_masks masks = { + .net = access_request, + }; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), masks, NULL); - if (!dom) + if (!subject) return 0; - if (WARN_ON_ONCE(dom->num_layers < 1)) - return -EACCES; if (!sk_is_tcp(sock->sk)) return 0; @@ -145,9 +141,10 @@ static int current_check_access_socket(struct socket *const sock, id.key.data = (__force uintptr_t)port; BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data)); - rule = landlock_find_rule(dom, id); - access_request = landlock_init_layer_masks( - dom, access_request, &layer_masks, LANDLOCK_KEY_NET_PORT); + rule = landlock_find_rule(subject->domain, id); + access_request = landlock_init_layer_masks(subject->domain, + access_request, &layer_masks, + LANDLOCK_KEY_NET_PORT); if (landlock_unmask_layers(rule, access_request, &layer_masks, ARRAY_SIZE(layer_masks))) return 0; From patchwork Sat Mar 8 18:44:02 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007703 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-42af.mail.infomaniak.ch (smtp-42af.mail.infomaniak.ch [84.16.66.175]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3172A1B041A for ; Sat, 8 Mar 2025 18:50:12 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=84.16.66.175 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459815; cv=none; b=R4Pxh/qk+ljgtzoN8fhQeUtkHlpO8LA1gHA6r3ywyjz6OglZ7Q//NcQzOPuEzDJfdcLeKtx0fw760rQTVn2j2nnmgsj+RDzaKRgIczZG7L3rUSDGfbnfMRUU5gxc1SKVMMfgbi0Q6daRtQfFcyZYz6psTklJ6VnXIXWZ8qh6upk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459815; c=relaxed/simple; bh=LAcwhyexj1ZfYg2BpS1Tj6Fb83PCQzM6VMckD2e+iFc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=gHdnXpFsFoY7qfGibFQxkM0rlRYyGdFOvhdKYVwktdTL8K24gnvL/j/MRNbR77XzfgITdOw/mcrNshMl/9cgW5d5LTE/X5ZRCmHYbxLzU/uYMZ1GSdgFiwpmc45rnX9tSvpNhkvzEm6NYjQwkce8zYMkwQbcuuYPS/LPJpk9Oqk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=KyIML9nA; arc=none smtp.client-ip=84.16.66.175 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="KyIML9nA" Received: from smtp-4-0001.mail.infomaniak.ch (unknown [IPv6:2001:1600:7:10:40ca:feff:fe05:1]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9BsQ167NzQgn; Sat, 8 Mar 2025 19:44:42 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459482; bh=UpfkxzrR33d21qbePQH9kdScV+WUPL3QSen9Fu3/hec=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=KyIML9nAWf85mKVNxRUX6+BWD8+DRwTx7/0t3jC80xJ7RkviRO6iNg9OwMUVxsjq/ RJPeT4ADHbnut7BWsXMyoMUKSl6x67skzDjXxtGcV1VzGk7h/yam9bXusfNW/bDQOb t1op0F46pKW/6FXPFJZPLiOJGmsFbjaE/oE7dIA8= Received: from unknown by smtp-4-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsP25hTzjnn; Sat, 8 Mar 2025 19:44:41 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 06/26] landlock: Prepare to use credential instead of domain for scope Date: Sat, 8 Mar 2025 19:44:02 +0100 Message-ID: <20250308184422.2159360-7-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha This cosmetic change that is needed for audit support, specifically to be able to filter according to cross-execution boundaries. Replace hardcoded LANDLOCK_SCOPE_SIGNAL with the signal_scope.scope variable. Use scoped guards for RCU read-side critical sections. Cc: Günther Noack Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-7-mic@digikod.net --- Changes since v4: - New patch. --- security/landlock/task.c | 50 +++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/security/landlock/task.c b/security/landlock/task.c index 98894ad1abc7..dbdfac11e015 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -7,6 +7,7 @@ */ #include +#include #include #include #include @@ -213,15 +214,15 @@ static int hook_unix_stream_connect(struct sock *const sock, struct sock *const other, struct sock *const newsk) { - const struct landlock_ruleset *const dom = - landlock_get_applicable_domain(landlock_get_current_domain(), - unix_scope); + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), unix_scope, + NULL); /* Quick return for non-landlocked tasks. */ - if (!dom) + if (!subject) return 0; - if (is_abstract_socket(other) && sock_is_scoped(other, dom)) + if (is_abstract_socket(other) && sock_is_scoped(other, subject->domain)) return -EPERM; return 0; @@ -230,11 +231,11 @@ static int hook_unix_stream_connect(struct sock *const sock, static int hook_unix_may_send(struct socket *const sock, struct socket *const other) { - const struct landlock_ruleset *const dom = - landlock_get_applicable_domain(landlock_get_current_domain(), - unix_scope); + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), unix_scope, + NULL); - if (!dom) + if (!subject) return 0; /* @@ -244,7 +245,8 @@ static int hook_unix_may_send(struct socket *const sock, if (unix_peer(sock->sk) == other->sk) return 0; - if (is_abstract_socket(other->sk) && sock_is_scoped(other->sk, dom)) + if (is_abstract_socket(other->sk) && + sock_is_scoped(other->sk, subject->domain)) return -EPERM; return 0; @@ -256,27 +258,27 @@ static const struct access_masks signal_scope = { static int hook_task_kill(struct task_struct *const p, struct kernel_siginfo *const info, const int sig, - const struct cred *const cred) + const struct cred *cred) { bool is_scoped; - const struct landlock_ruleset *dom; + const struct landlock_cred_security *subject; - if (cred) { - /* Dealing with USB IO. */ - dom = landlock_cred(cred)->domain; - } else { - dom = landlock_get_current_domain(); - } - dom = landlock_get_applicable_domain(dom, signal_scope); + if (!cred) + /* Not dealing with USB IO. */ + cred = current_cred(); + + subject = landlock_get_applicable_subject(cred, signal_scope, NULL); /* Quick return for non-landlocked tasks. */ - if (!dom) + if (!subject) return 0; - rcu_read_lock(); - is_scoped = domain_is_scoped(dom, landlock_get_task_domain(p), - LANDLOCK_SCOPE_SIGNAL); - rcu_read_unlock(); + scoped_guard(rcu) + { + is_scoped = domain_is_scoped(subject->domain, + landlock_get_task_domain(p), + signal_scope.scope); + } if (is_scoped) return -EPERM; From patchwork Sat Mar 8 18:44:03 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007676 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-1908.mail.infomaniak.ch (smtp-1908.mail.infomaniak.ch [185.125.25.8]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8DCD22116F6 for ; Sat, 8 Mar 2025 18:44:50 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.125.25.8 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459493; cv=none; b=B+aajYXckIAPxsxkgu0wR+noTaKDQezQ/8xjNwXJXuEztybuh2x7/ac2Ll//W7Q23zrEfvq5OavpazZDEYEbsg/qZycdJwn6AI0aQv2Wm7AF71x2p5V77xit4AdC/NBRxCh2GK6BdS+FdFz+2A9uoCFq7nRB6myAnmVLkPB9fLU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459493; c=relaxed/simple; bh=teYppnQilIsd+YguFxHHDZ7fudTUmR5JFh8koioF+Pc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=WmUqRhsjypl56D1+TXrWd5xLqwadbgZLORyKZf2us0qNW8Lt69lzzGGqtgh/YXnN8skjsf02bvqWppE9Deped6dFAxdR6lfqkpAjxZ5u3oywSa1+J4PlLktXswhilvV2YWOn1Ve5+LYMZRA4CTrUoq/nfKhwLR3pcmwMP5FI7Mk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=W2elPVWU; arc=none smtp.client-ip=185.125.25.8 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="W2elPVWU" Received: from smtp-3-0000.mail.infomaniak.ch (unknown [IPv6:2001:1600:4:17::246b]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9BsR3XywzRxY; Sat, 8 Mar 2025 19:44:43 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459483; bh=SBOoMihG9Qi1HSfwunqx1u8g1OCHU2vV2AsORwv30PY=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=W2elPVWU/RH563idNt1A6X7lYeaViAOxMecM00vpIssQzd8pVorhXlKMpmmzRmQzO kadlQsawyhhyQII5+6dIEIchhXvUTuzRBecsfDdABtHIzHOMJbaOPATaqTJ7TypgMX IRKkAKACY9WmoW+wvX5QyJqTKVvtVY8UpBwIJfWM= Received: from unknown by smtp-3-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsQ5WWxzqjc; Sat, 8 Mar 2025 19:44:42 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 07/26] landlock: Prepare to use credential instead of domain for fowner Date: Sat, 8 Mar 2025 19:44:03 +0100 Message-ID: <20250308184422.2159360-8-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha This cosmetic change that is needed for audit support, specifically to be able to filter according to cross-execution boundaries. struct landlock_file_security's size stay the same for now but it will increase with struct landlock_cred_security's size. Only save Landlock domain in hook_file_set_fowner() if the current domain has LANDLOCK_SCOPE_SIGNAL, which was previously done for each hook_file_send_sigiotask() calls. This should improve a bit performances. Replace hardcoded LANDLOCK_SCOPE_SIGNAL with the signal_scope.scope variable. Use scoped guards for RCU read-side critical sections. Cc: Günther Noack Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-8-mic@digikod.net --- Changes since v5: - Only call landlock_get_ruleset() when needed in hook_file_set_fowner(). Changes since v4: - New patch. --- security/landlock/fs.c | 26 ++++++++++++++++++++------ security/landlock/fs.h | 12 +++++++----- security/landlock/task.c | 25 ++++++++++++++++--------- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/security/landlock/fs.c b/security/landlock/fs.c index d5b153d29fcb..7dbfc6420e1b 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1641,17 +1641,31 @@ static int hook_file_ioctl_compat(struct file *file, unsigned int cmd, static void hook_file_set_fowner(struct file *file) { - struct landlock_ruleset *new_dom, *prev_dom; + static const struct access_masks signal_scope = { + .scope = LANDLOCK_SCOPE_SIGNAL, + }; + const struct landlock_cred_security *new_subject; + struct landlock_cred_security *fown_subject; + struct landlock_ruleset *prev_dom; /* * Lock already held by __f_setown(), see commit 26f204380a3c ("fs: Fix * file_set_fowner LSM hook inconsistencies"). */ lockdep_assert_held(&file_f_owner(file)->lock); - new_dom = landlock_get_current_domain(); - landlock_get_ruleset(new_dom); - prev_dom = landlock_file(file)->fown_domain; - landlock_file(file)->fown_domain = new_dom; + + fown_subject = &landlock_file(file)->fown_subject; + prev_dom = fown_subject->domain; + new_subject = landlock_get_applicable_subject(current_cred(), + signal_scope, NULL); + if (new_subject) { + landlock_get_ruleset(new_subject->domain); + *fown_subject = *new_subject; + } else { + static const struct landlock_cred_security empty = {}; + + *fown_subject = empty; + } /* Called in an RCU read-side critical section. */ landlock_put_ruleset_deferred(prev_dom); @@ -1659,7 +1673,7 @@ static void hook_file_set_fowner(struct file *file) static void hook_file_free_security(struct file *file) { - landlock_put_ruleset_deferred(landlock_file(file)->fown_domain); + landlock_put_ruleset_deferred(landlock_file(file)->fown_subject.domain); } static struct security_hook_list landlock_hooks[] __ro_after_init = { diff --git a/security/landlock/fs.h b/security/landlock/fs.h index d445f411c26a..1449a90e92c7 100644 --- a/security/landlock/fs.h +++ b/security/landlock/fs.h @@ -14,6 +14,7 @@ #include #include "access.h" +#include "cred.h" #include "ruleset.h" #include "setup.h" @@ -54,12 +55,13 @@ struct landlock_file_security { */ access_mask_t allowed_access; /** - * @fown_domain: Domain of the task that set the PID that may receive a - * signal e.g., SIGURG when writing MSG_OOB to the related socket. - * This pointer is protected by the related file->f_owner->lock, as for - * fown_struct's members: pid, uid, and euid. + * @fown_subject: Landlock credential of the task that set the PID that + * may receive a signal e.g., SIGURG when writing MSG_OOB to the + * related socket. This pointer is protected by the related + * file->f_owner->lock, as for fown_struct's members: pid, uid, and + * euid. */ - struct landlock_ruleset *fown_domain; + struct landlock_cred_security fown_subject; }; /** diff --git a/security/landlock/task.c b/security/landlock/task.c index dbdfac11e015..da8f82c8054a 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -288,22 +288,29 @@ static int hook_task_kill(struct task_struct *const p, static int hook_file_send_sigiotask(struct task_struct *tsk, struct fown_struct *fown, int signum) { - const struct landlock_ruleset *dom; + const struct landlock_cred_security *subject; bool is_scoped = false; /* Lock already held by send_sigio() and send_sigurg(). */ lockdep_assert_held(&fown->lock); - dom = landlock_get_applicable_domain( - landlock_file(fown->file)->fown_domain, signal_scope); + subject = &landlock_file(fown->file)->fown_subject; - /* Quick return for unowned socket. */ - if (!dom) + /* + * Quick return for unowned socket. + * + * subject->domain has already been filtered when saved by + * hook_file_set_fowner(), so there is no need to call + * landlock_get_applicable_subject() here. + */ + if (!subject->domain) return 0; - rcu_read_lock(); - is_scoped = domain_is_scoped(dom, landlock_get_task_domain(tsk), - LANDLOCK_SCOPE_SIGNAL); - rcu_read_unlock(); + scoped_guard(rcu) + { + is_scoped = domain_is_scoped(subject->domain, + landlock_get_task_domain(tsk), + signal_scope.scope); + } if (is_scoped) return -EPERM; From patchwork Sat Mar 8 18:44:04 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007704 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-190a.mail.infomaniak.ch (smtp-190a.mail.infomaniak.ch [185.125.25.10]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6644615B54C for ; Sat, 8 Mar 2025 18:50:13 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.125.25.10 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459815; cv=none; b=L0vHht8ICh3RM4kpxxCmwQt8xzeWySYqFzPuAFaoEIelYbZ+AjWTvTTE6Ur02uWHUHyX+V0y3sYEmVRDzAYFlORSN6ofgwDQyGUy6Fbs4S6GdWMHUYXUJK0qmSzHH+2c/yVkrFBfRkE1JG0nkXQmc6CmpqwbOlK21Fiz7RwDjKM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459815; c=relaxed/simple; bh=KYgdmaFLVxr4Nl1672jOHSH4mVTZf+6+AlybgZlnzJc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=aqSehGK5e4tfhA5DphPRM1Zr/r3adgaRFA9S/luUS/pj8VpeVvChZEh6/FcNy+hklwPpsSiQQESbgmdOCZHk/t2m4IvnpTC++8+wDKWyEkJzIESEKUXN6iwcF0qdkTrImtwpuj9ygcjBxEigNJJfX4iX3h6oPAs3Obu8kGWmyBQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=aUqX/Ixj; arc=none smtp.client-ip=185.125.25.10 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="aUqX/Ixj" Received: from smtp-3-0000.mail.infomaniak.ch (smtp-3-0000.mail.infomaniak.ch [10.4.36.107]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9BsS6JV8zNrt; Sat, 8 Mar 2025 19:44:44 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459484; bh=w+iB3Ja2zMoCMbnIyQGhhP5gE1y+T8FmeGt8Bxld6jA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=aUqX/IxjbdUnsw8qS1fKlpM/OtrETjXdizkvKVl+kitbzI47tWrBVIPhLeN158XT9 3GAA5KzThdNlwdIWVdCTlARtHPeJXQTIHWmRtKP8pwvQSssM4CcRa3JwBD4WLAJKDa bA+64M0VIg8sFESmERAQv5hXCKQ+LjdW2YZn9QY0= Received: from unknown by smtp-3-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsS11vTzsl6; Sat, 8 Mar 2025 19:44:44 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 08/26] landlock: Identify domain execution crossing Date: Sat, 8 Mar 2025 19:44:04 +0100 Message-ID: <20250308184422.2159360-9-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Extend struct landlock_cred_security with a domain_exec bitmask to identify which Landlock domain were created by the current task's bprm. This is reset on each execve(2) call. Cc: Günther Noack Cc: Paul Moore Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-9-mic@digikod.net --- Changes since v5: - Add documentation and pack struct landlock_cred_security to minimize struct landlock_file_security. Changes since v4: - New patch. --- security/landlock/cred.c | 26 ++++++++++++++++++++++---- security/landlock/cred.h | 32 +++++++++++++++++++++++++++++++- security/landlock/syscalls.c | 5 +++++ 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/security/landlock/cred.c b/security/landlock/cred.c index db9fe7d906ba..a22756fe3b71 100644 --- a/security/landlock/cred.c +++ b/security/landlock/cred.c @@ -4,8 +4,10 @@ * * Copyright © 2017-2020 Mickaël Salaün * Copyright © 2018-2020 ANSSI + * Copyright © 2025 Microsoft Corporation */ +#include #include #include @@ -17,11 +19,12 @@ static void hook_cred_transfer(struct cred *const new, const struct cred *const old) { - struct landlock_ruleset *const old_dom = landlock_cred(old)->domain; + const struct landlock_cred_security *const old_llcred = + landlock_cred(old); - if (old_dom) { - landlock_get_ruleset(old_dom); - landlock_cred(new)->domain = old_dom; + if (old_llcred->domain) { + landlock_get_ruleset(old_llcred->domain); + *landlock_cred(new) = *old_llcred; } } @@ -40,10 +43,25 @@ static void hook_cred_free(struct cred *const cred) landlock_put_ruleset_deferred(dom); } +#ifdef CONFIG_AUDIT + +static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm) +{ + /* Resets for each execution. */ + landlock_cred(bprm->cred)->domain_exec = 0; + return 0; +} + +#endif /* CONFIG_AUDIT */ + static struct security_hook_list landlock_hooks[] __ro_after_init = { LSM_HOOK_INIT(cred_prepare, hook_cred_prepare), LSM_HOOK_INIT(cred_transfer, hook_cred_transfer), LSM_HOOK_INIT(cred_free, hook_cred_free), + +#ifdef CONFIG_AUDIT + LSM_HOOK_INIT(bprm_creds_for_exec, hook_bprm_creds_for_exec), +#endif /* CONFIG_AUDIT */ }; __init void landlock_add_cred_hooks(void) diff --git a/security/landlock/cred.h b/security/landlock/cred.h index fdbbaf66d151..cf38caf77adc 100644 --- a/security/landlock/cred.h +++ b/security/landlock/cred.h @@ -9,17 +9,47 @@ #ifndef _SECURITY_LANDLOCK_CRED_H #define _SECURITY_LANDLOCK_CRED_H +#include #include #include #include #include "access.h" +#include "limits.h" #include "ruleset.h" #include "setup.h" +/** + * struct landlock_cred_security - Credential security blob + * + * This structure is packed to minimize the size of struct + * landlock_file_security. However, it is always aligned in the LSM cred blob, + * see lsm_set_blob_size(). + */ struct landlock_cred_security { + /** + * @domain: Immutable ruleset enforced on a task. + */ struct landlock_ruleset *domain; -}; + +#ifdef CONFIG_AUDIT + /** + * @domain_exec: Bitmask identifying the domain layers that were enforced by + * the current task's executed file (i.e. no new execve(2) since + * landlock_restrict_self(2)). + */ + u16 domain_exec; +#endif /* CONFIG_AUDIT */ +} __packed; + +#ifdef CONFIG_AUDIT + +/* Makes sure all layer executions can be stored. */ +static_assert(BITS_PER_TYPE(typeof_member(struct landlock_cred_security, + domain_exec)) >= + LANDLOCK_MAX_NUM_LAYERS); + +#endif /* CONFIG_AUDIT */ static inline struct landlock_cred_security * landlock_cred(const struct cred *cred) diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index a9760d252fc2..5129981fec8b 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -496,5 +496,10 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, /* Replaces the old (prepared) domain. */ landlock_put_ruleset(new_llcred->domain); new_llcred->domain = new_dom; + +#ifdef CONFIG_AUDIT + new_llcred->domain_exec |= 1 << (new_dom->num_layers - 1); +#endif /* CONFIG_AUDIT */ + return commit_creds(new_cred); } From patchwork Sat Mar 8 18:44:05 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007678 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-42a8.mail.infomaniak.ch (smtp-42a8.mail.infomaniak.ch [84.16.66.168]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8A712214203; Sat, 8 Mar 2025 18:44:53 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=84.16.66.168 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459496; cv=none; b=dJk98ouEZpQ4cqo/o3Yl0ij5QfYVh/h0ThiDWuV76g0h1eHxHHO4ZbGPqxu/7Sd+e/g/YjA63st2PKEe9tMf2jGURN9AsLPHJJ2XiE1CP0tttOkCGnwEZSaebAZcpdYHoC3+Waik7SsOW0yQwLTPAg2ekIioVvG7UNCukqtw8Tc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459496; c=relaxed/simple; bh=qYsJ55J+gH40nTYTq7KYAy0uiwXf1MUoCS70Z3qvNvw=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=fPZce9YlahVwDDYjaylJ/3X4MDBJf0NWsUtZEX7TDhHoAf6OE9R+RoUrdBmcNNuJdUq/vvr/S67x7iC5+oAYVMNZk/RiT5XIrSOnPTtvdoed0xk8x93pQ1/aiiVxWa3HalXVYMlBpQl3SOzCwnWKYsmnQt30QdsrUGDOAvngemw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=gb41c+SE; arc=none smtp.client-ip=84.16.66.168 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="gb41c+SE" Received: from smtp-3-0001.mail.infomaniak.ch (smtp-3-0001.mail.infomaniak.ch [10.4.36.108]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9BsV4MqXzNs1; Sat, 8 Mar 2025 19:44:46 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459486; bh=QiKP7b/Dm/KNRWq66l/HuRjtqw6GR5zzVNgMzJzHzfA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=gb41c+SECvufq/u5Bl17wifFXuUFivpZNW8Fgzhn26fUmhM9dT7K7Bv7NwGv0UD4w M9+5N8qwq9E5+uY4rodCilHZzd7R064osG6idwbqxDCPnGWI3LSH2cIklabc4J2MzZ DceGWZbszvRyo7jnGCm9KGPUVWc71li0GmFGcf3k= Received: from unknown by smtp-3-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsT3pPPzGx; Sat, 8 Mar 2025 19:44:45 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 09/26] landlock: Add AUDIT_LANDLOCK_ACCESS and log ptrace denials Date: Sat, 8 Mar 2025 19:44:05 +0100 Message-ID: <20250308184422.2159360-10-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add a new AUDIT_LANDLOCK_ACCESS record type dedicated to an access request denied by a Landlock domain. AUDIT_LANDLOCK_ACCESS indicates that something unexpected happened. For now, only denied access are logged, which means that any AUDIT_LANDLOCK_ACCESS record is always followed by a SYSCALL record with "success=no". However, log parsers should check this syscall property because this is the only sign that a request was denied. Indeed, we could have "success=yes" if Landlock would support a "permissive" mode. We could also add a new field for this mode to AUDIT_LANDLOCK_DOMAIN (see following commit). By default, the only logged access requests are those coming from the same executed program that enforced the Landlock restriction on itself. In other words, no audit record are created for a task after it called execve(2). This is required to avoid log spam because programs may only be aware of their own restrictions, but not the inherited ones. Following commits will allow to conditionally generate AUDIT_LANDLOCK_ACCESS records according to dedicated landlock_restrict_self(2)'s flags. The AUDIT_LANDLOCK_ACCESS message contains: - the "domain" ID restricting the action on an object, - the "blockers" that are missing to allow the requested access, - a set of fields identifying the related object (e.g. task identified with "opid" and "ocomm"). The blockers are implicit restrictions (e.g. ptrace), or explicit access rights (e.g. filesystem), or explicit scopes (e.g. signal). This field contains a list of at least one element, each separated with a comma. The initial blocker is "ptrace", which describe all implicit Landlock restrictions related to ptrace (e.g. deny tracing of tasks outside a sandbox). Add audit support to ptrace_access_check and ptrace_traceme hooks. For the ptrace_access_check case, we log the current/parent domain and the child task. For the ptrace_traceme case, we log the parent domain and the parent task. Indeed, the requester is the current task, but the action would be performed by the parent task. Audit event sample: type=LANDLOCK_ACCESS msg=audit(1729738800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd" type=SYSCALL msg=audit(1729738800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0 A following commit adds user documentation. Add KUnit tests to check reading of domain ID relative to layer level. The quick return for non-landlocked tasks is moved from task_ptrace() to each LSM hooks. Because the landlock_log_denial() function is only called when an access is denied, the compiler should be able to optimize the struct landlock_request initializations. It is not useful to inline the audit_enabled check because other computation are performed anyway, and by the same landlock_log_denia() code. Use scoped guards for RCU read-side critical sections. Cc: Günther Noack Acked-by: Paul Moore (Audit) Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-10-mic@digikod.net --- Changes since v5: - Add Acked-by Paul. - Move request declarations in the landlock_log_denial() calls to not impact allowed requests with audit, and return as soon as possible when access is allowed. - Remove the audit_context() check in landlock_log_denial() because the context may not already exist. This issue was identified thanks to a new test. - Remove unlikely(). Changes since v4: - Rename AUDIT_LANDLOCK_DENY to AUDIT_LANDLOCK_ACCESS, requested by Paul. - Make landlock_log_denial() get Landlock credential instead of Landlock domain to be able to filter on the domain_exe variable. - Rebase on top of the migration from struct landlock_ruleset to struct landlock_cred_security. - Rename landlock_init_current_hierarchy() to landlock_init_hierarchy_log(). - Rebase on top of the scoped guard patches. - By default, do not log denials after an execution. - Use scoped guards for RCU read-side critical sections. Changes since v3: - Extend commit message. Changes since v2: - Log domain IDs as hexadecimal number: this is a more compact notation (i.e. at least one less digit), it improves alignment in logs, and it makes most IDs start with 1 as leading digit (because of the 2^32 minimal value). Do not use the "0x" prefix that would add useless data to logs. - Constify function arguments. - Clean up Makefile entries. Changes since v1: - Move most audit code to this patch. - Rebase on the TCP patch series. - Don't log missing access right: simplify and make it generic for rule types. - Don't log errno and then don't wrap the error with landlock_log_request(), as suggested by Jeff. - Add a WARN_ON_ONCE() check to never dereference null pointers. - Only log when audit is enabled. - Don't log task's PID/TID with log_task() because it would be redundant with the SYSCALL record. - Move the "op" in front and rename "domain" to "denying_domain" to make it more consistent with other entries. - Don't update the request with the domain ID but add an helper to get it from the layer masks (and in a following commit with a struct file). - Revamp get_domain_id_from_layer_masks() into get_level_from_layer_masks(). - For ptrace_traceme, log the parent domain instead of the current one. - Add documentation. - Rename AUDIT_LANDLOCK_DENIAL to AUDIT_LANDLOCK_DENY. - Only log the domain ID and the target task. - Log "blockers", which are either implicit restrictions (e.g. ptrace) or explicit access rights (e.g. filesystem), or scopes (e.g. signal). - Don't log LSM hook names/operations. - Pick an audit event ID folling the IPE ones. - Add KUnit tests. --- include/uapi/linux/audit.h | 3 +- security/landlock/Makefile | 5 +- security/landlock/audit.c | 146 ++++++++++++++++++++++++++++++++++++ security/landlock/audit.h | 53 +++++++++++++ security/landlock/domain.c | 28 +++++++ security/landlock/domain.h | 22 ++++++ security/landlock/ruleset.c | 6 ++ security/landlock/task.c | 96 ++++++++++++++++++------ 8 files changed, 334 insertions(+), 25 deletions(-) create mode 100644 security/landlock/audit.c create mode 100644 security/landlock/audit.h create mode 100644 security/landlock/domain.c diff --git a/include/uapi/linux/audit.h b/include/uapi/linux/audit.h index d9a069b4a775..5dd53f416a4a 100644 --- a/include/uapi/linux/audit.h +++ b/include/uapi/linux/audit.h @@ -33,7 +33,7 @@ * 1100 - 1199 user space trusted application messages * 1200 - 1299 messages internal to the audit daemon * 1300 - 1399 audit event messages - * 1400 - 1499 SE Linux use + * 1400 - 1499 access control messages * 1500 - 1599 kernel LSPP events * 1600 - 1699 kernel crypto events * 1700 - 1799 kernel anomaly records @@ -146,6 +146,7 @@ #define AUDIT_IPE_ACCESS 1420 /* IPE denial or grant */ #define AUDIT_IPE_CONFIG_CHANGE 1421 /* IPE config change */ #define AUDIT_IPE_POLICY_LOAD 1422 /* IPE policy load */ +#define AUDIT_LANDLOCK_ACCESS 1423 /* Landlock denial */ #define AUDIT_FIRST_KERN_ANOM_MSG 1700 #define AUDIT_LAST_KERN_ANOM_MSG 1799 diff --git a/security/landlock/Makefile b/security/landlock/Makefile index e1777abbc413..3160c2bdac1d 100644 --- a/security/landlock/Makefile +++ b/security/landlock/Makefile @@ -5,4 +5,7 @@ landlock-y := setup.o syscalls.o object.o ruleset.o \ landlock-$(CONFIG_INET) += net.o -landlock-$(CONFIG_AUDIT) += id.o +landlock-$(CONFIG_AUDIT) += \ + id.o \ + audit.o \ + domain.o diff --git a/security/landlock/audit.c b/security/landlock/audit.c new file mode 100644 index 000000000000..8c7c71386a66 --- /dev/null +++ b/security/landlock/audit.c @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock LSM - Audit helpers + * + * Copyright © 2023-2025 Microsoft Corporation + */ + +#include +#include +#include + +#include "audit.h" +#include "cred.h" +#include "domain.h" +#include "ruleset.h" + +static const char *get_blocker(const enum landlock_request_type type) +{ + switch (type) { + case LANDLOCK_REQUEST_PTRACE: + return "ptrace"; + } + + WARN_ON_ONCE(1); + return "unknown"; +} + +static void log_blockers(struct audit_buffer *const ab, + const enum landlock_request_type type) +{ + audit_log_format(ab, "%s", get_blocker(type)); +} + +static struct landlock_hierarchy * +get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer) +{ + struct landlock_hierarchy *node = domain->hierarchy; + ssize_t i; + + if (WARN_ON_ONCE(layer >= domain->num_layers)) + return node; + + for (i = domain->num_layers - 1; i > layer; i--) { + if (WARN_ON_ONCE(!node->parent)) + break; + + node = node->parent; + } + + return node; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_get_hierarchy(struct kunit *const test) +{ + struct landlock_hierarchy dom0_node = { + .id = 10, + }; + struct landlock_hierarchy dom1_node = { + .parent = &dom0_node, + .id = 20, + }; + struct landlock_hierarchy dom2_node = { + .parent = &dom1_node, + .id = 30, + }; + struct landlock_ruleset dom2 = { + .hierarchy = &dom2_node, + .num_layers = 3, + }; + + KUNIT_EXPECT_EQ(test, 10, get_hierarchy(&dom2, 0)->id); + KUNIT_EXPECT_EQ(test, 20, get_hierarchy(&dom2, 1)->id); + KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, 2)->id); + KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, -1)->id); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +static bool is_valid_request(const struct landlock_request *const request) +{ + if (WARN_ON_ONCE(!request->layer_plus_one)) + return false; + + return true; +} + +/** + * landlock_log_denial - Create audit records related to a denial + * + * @subject: The Landlock subject's credential denying an action. + * @request: Detail of the user space request. + */ +void landlock_log_denial(const struct landlock_cred_security *const subject, + const struct landlock_request *const request) +{ + struct audit_buffer *ab; + struct landlock_hierarchy *youngest_denied; + size_t youngest_layer; + + if (WARN_ON_ONCE(!subject || !subject->domain || + !subject->domain->hierarchy || !request)) + return; + + if (!is_valid_request(request)) + return; + + if (!audit_enabled) + return; + + youngest_layer = request->layer_plus_one - 1; + youngest_denied = get_hierarchy(subject->domain, youngest_layer); + + /* Ignores denials after an execution. */ + if (!(subject->domain_exec & (1 << youngest_layer))) + return; + + ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN, + AUDIT_LANDLOCK_ACCESS); + if (!ab) + return; + + audit_log_format(ab, "domain=%llx blockers=", youngest_denied->id); + log_blockers(ab, request->type); + audit_log_lsm_data(ab, &request->audit); + audit_log_end(ab); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static struct kunit_case test_cases[] = { + /* clang-format off */ + KUNIT_CASE(test_get_hierarchy), + {} + /* clang-format on */ +}; + +static struct kunit_suite test_suite = { + .name = "landlock_audit", + .test_cases = test_cases, +}; + +kunit_test_suite(test_suite); + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ diff --git a/security/landlock/audit.h b/security/landlock/audit.h new file mode 100644 index 000000000000..daca14d77649 --- /dev/null +++ b/security/landlock/audit.h @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Audit helpers + * + * Copyright © 2023-2025 Microsoft Corporation + */ + +#ifndef _SECURITY_LANDLOCK_AUDIT_H +#define _SECURITY_LANDLOCK_AUDIT_H + +#include +#include + +#include "cred.h" +#include "ruleset.h" + +enum landlock_request_type { + LANDLOCK_REQUEST_PTRACE = 1, +}; + +/* + * We should be careful to only use a variable of this type for + * landlock_log_denial(). This way, the compiler can remove it entirely if + * CONFIG_AUDIT is not set. + */ +struct landlock_request { + /* Mandatory fields. */ + enum landlock_request_type type; + struct common_audit_data audit; + + /** + * layer_plus_one: First layer level that denies the request + 1. The + * extra one is useful to detect uninitialized field. + */ + size_t layer_plus_one; +}; + +#ifdef CONFIG_AUDIT + +void landlock_log_denial(const struct landlock_cred_security *const subject, + const struct landlock_request *const request); + +#else /* CONFIG_AUDIT */ + +static inline void +landlock_log_denial(const struct landlock_cred_security *const subject, + const struct landlock_request *const request) +{ +} + +#endif /* CONFIG_AUDIT */ + +#endif /* _SECURITY_LANDLOCK_AUDIT_H */ diff --git a/security/landlock/domain.c b/security/landlock/domain.c new file mode 100644 index 000000000000..f6877ae79380 --- /dev/null +++ b/security/landlock/domain.c @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock LSM - Domain management + * + * Copyright © 2016-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation + */ + +#include "domain.h" +#include "id.h" + +#ifdef CONFIG_AUDIT + +/** + * landlock_init_hierarchy_log - Partially initialize landlock_hierarchy + * + * @hierarchy: The hierarchy to initialize. + * + * @hierarchy->parent and @hierarchy->usage should already be set. + */ +int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) +{ + hierarchy->id = landlock_get_id_range(1); + return 0; +} + +#endif /* CONFIG_AUDIT */ diff --git a/security/landlock/domain.h b/security/landlock/domain.h index 015d61fd81ec..1020878180d3 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -4,6 +4,7 @@ * * Copyright © 2016-2020 Mickaël Salaün * Copyright © 2018-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation */ #ifndef _SECURITY_LANDLOCK_DOMAIN_H @@ -26,6 +27,13 @@ struct landlock_hierarchy { * domain. */ refcount_t usage; + +#ifdef CONFIG_AUDIT + /** + * @id: Landlock domain ID, sets once at domain creation time. + */ + u64 id; +#endif /* CONFIG_AUDIT */ }; static inline void @@ -45,4 +53,18 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy) } } +#ifdef CONFIG_AUDIT + +int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy); + +#else /* CONFIG_AUDIT */ + +static inline int +landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) +{ + return 0; +} + +#endif /* CONFIG_AUDIT */ + #endif /* _SECURITY_LANDLOCK_DOMAIN_H */ diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index adb7f87828df..f273a40e9780 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -23,6 +23,7 @@ #include #include "access.h" +#include "audit.h" #include "domain.h" #include "limits.h" #include "object.h" @@ -505,6 +506,7 @@ static void free_ruleset_work(struct work_struct *const work) free_ruleset(ruleset); } +/* Only called by hook_cred_free(). */ void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset) { if (ruleset && refcount_dec_and_test(&ruleset->usage)) { @@ -564,6 +566,10 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent, if (err) return ERR_PTR(err); + err = landlock_init_hierarchy_log(new_dom->hierarchy); + if (err) + return ERR_PTR(err); + return no_free_ptr(new_dom); } diff --git a/security/landlock/task.c b/security/landlock/task.c index da8f82c8054a..e8a0b4ead381 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -11,12 +11,14 @@ #include #include #include +#include #include #include #include #include #include +#include "audit.h" #include "common.h" #include "cred.h" #include "domain.h" @@ -39,41 +41,29 @@ static bool domain_scope_le(const struct landlock_ruleset *const parent, { const struct landlock_hierarchy *walker; + /* Quick return for non-landlocked tasks. */ if (!parent) return true; + if (!child) return false; + for (walker = child->hierarchy; walker; walker = walker->parent) { if (walker == parent->hierarchy) /* @parent is in the scoped hierarchy of @child. */ return true; } + /* There is no relationship between @parent and @child. */ return false; } -static bool task_is_scoped(const struct task_struct *const parent, - const struct task_struct *const child) -{ - bool is_scoped; - const struct landlock_ruleset *dom_parent, *dom_child; - - rcu_read_lock(); - dom_parent = landlock_get_task_domain(parent); - dom_child = landlock_get_task_domain(child); - is_scoped = domain_scope_le(dom_parent, dom_child); - rcu_read_unlock(); - return is_scoped; -} - -static int task_ptrace(const struct task_struct *const parent, - const struct task_struct *const child) +static int domain_ptrace(const struct landlock_ruleset *const parent, + const struct landlock_ruleset *const child) { - /* Quick return for non-landlocked tasks. */ - if (!landlocked(parent)) - return 0; - if (task_is_scoped(parent, child)) + if (domain_scope_le(parent, child)) return 0; + return -EPERM; } @@ -93,7 +83,39 @@ static int task_ptrace(const struct task_struct *const parent, static int hook_ptrace_access_check(struct task_struct *const child, const unsigned int mode) { - return task_ptrace(current, child); + const struct landlock_cred_security *parent_subject; + const struct landlock_ruleset *child_dom; + int err; + + /* Quick return for non-landlocked tasks. */ + parent_subject = landlock_cred(current_cred()); + if (!parent_subject) + return 0; + + scoped_guard(rcu) + { + child_dom = landlock_get_task_domain(child); + err = domain_ptrace(parent_subject->domain, child_dom); + } + + if (!err) + return 0; + + /* + * For the ptrace_access_check case, we log the current/parent domain + * and the child task. + */ + if (!(mode & PTRACE_MODE_NOAUDIT)) + landlock_log_denial(parent_subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_PTRACE, + .audit = { + .type = LSM_AUDIT_DATA_TASK, + .u.tsk = child, + }, + .layer_plus_one = parent_subject->domain->num_layers, + }); + + return err; } /** @@ -110,7 +132,35 @@ static int hook_ptrace_access_check(struct task_struct *const child, */ static int hook_ptrace_traceme(struct task_struct *const parent) { - return task_ptrace(parent, current); + const struct landlock_cred_security *parent_subject; + const struct landlock_ruleset *child_dom; + int err; + + child_dom = landlock_get_current_domain(); + + guard(rcu)(); + parent_subject = landlock_cred(__task_cred(parent)); + err = domain_ptrace(parent_subject->domain, child_dom); + + if (!err) + return 0; + + /* + * For the ptrace_traceme case, we log the domain which is the cause of + * the denial, which means the parent domain instead of the current + * domain. This may look weird because the ptrace_traceme action is a + * request to be traced, but the semantic is consistent with + * hook_ptrace_access_check(). + */ + landlock_log_denial(parent_subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_PTRACE, + .audit = { + .type = LSM_AUDIT_DATA_TASK, + .u.tsk = parent, + }, + .layer_plus_one = parent_subject->domain->num_layers, + }); + return err; } /** @@ -129,7 +179,7 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client, access_mask_t scope) { int client_layer, server_layer; - struct landlock_hierarchy *client_walker, *server_walker; + const struct landlock_hierarchy *client_walker, *server_walker; /* Quick return if client has no domain */ if (WARN_ON_ONCE(!client)) From patchwork Sat Mar 8 18:44:06 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007675 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-190c.mail.infomaniak.ch (smtp-190c.mail.infomaniak.ch [185.125.25.12]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C96AB20F07F for ; Sat, 8 Mar 2025 18:44:49 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.125.25.12 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459492; cv=none; b=U/3JXMgaYQ3dMBlxcZsGwLqWXDB9J9katFmT2a+fWuzpu7JY8FbV8eyP4qDk+d4Yy0LBbOFeTBX+JkIX8PgH6fXBxJpeg9kMEmryPtuwx5ZEgvTWtyGciP4ZYz4L/kHj4HrOcLanWLO/IW/Wt86mElgz40NRtM6yCy2BHWE9H4A= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459492; c=relaxed/simple; bh=hbB/AYjtyvCw9Vp0iu4w07VB0UaZxuAsHpEh9J9gQgY=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=hYCBqfoN16LGt3vQzi2VrDV6Dn7uwp2e0UWeqvL6Qw9e4RWINzrWnoe3XOzLQmmAAOKO7rjiWWXxJpodsfQ1Rpx638ntnrFCzH2Xw6dU8GMpikJxExSQtVCfXIoYDdjDiOjLJHR+5Q7p+L4X8tLmRiuEJPXYcpUzvXN36Yz+nD4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=h11sNoue; arc=none smtp.client-ip=185.125.25.12 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="h11sNoue" Received: from smtp-3-0001.mail.infomaniak.ch (unknown [IPv6:2001:1600:4:17::246c]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9BsX08q2zQwr; Sat, 8 Mar 2025 19:44:48 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459487; bh=Wqhtx5Y9KB6y9M9b0Le6NOgNXDlkkhBDFFt7UwoaaTA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=h11sNoueSCL45/9HxGbGWe2QCUmLec/D7yaKql+SuVDwuy8Wc9tLlnu4oCVEjdUCb 6EJJUQwVitmakaiDoIAvz0UZJwkxV7EBR1CzEwL737W4kZnB5UYqgjenIja5KZOiaq 6vZ0rdnverFjXwoKNKeLc11DDX5Vin4C72ewbUec= Received: from unknown by smtp-3-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsW1WfYz1gJ; Sat, 8 Mar 2025 19:44:47 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 10/26] landlock: Add AUDIT_LANDLOCK_DOMAIN and log domain status Date: Sat, 8 Mar 2025 19:44:06 +0100 Message-ID: <20250308184422.2159360-11-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Asynchronously log domain information when it first denies an access. This minimize the amount of generated logs, which makes it possible to always log denials for the current execution since they should not happen. These records are identified with the new AUDIT_LANDLOCK_DOMAIN type. The AUDIT_LANDLOCK_DOMAIN message contains: - the "domain" ID which is described; - the "status" which can either be "allocated" or "deallocated"; - the "mode" which is for now only "enforcing"; - for the "allocated" status, a minimal set of properties to easily identify the task that loaded the domain's policy with landlock_restrict_self(2): "pid", "uid", executable path ("exe"), and command line ("comm"); - for the "deallocated" state, the number of "denials" accounted to this domain, which is at least 1. This requires each domain to save these task properties at creation time in the new struct landlock_details. A reference to the PID is kept for the lifetime of the domain to avoid race conditions when investigating the related task. The executable path is resolved and stored to not keep a reference to the filesystem and block related actions. All these metadata are stored for the lifetime of the related domain and should then be minimal. The required memory is not accounted to the task calling landlock_restrict_self(2) contrary to most other Landlock allocations (see related comment). The AUDIT_LANDLOCK_DOMAIN record follows the first AUDIT_LANDLOCK_ACCESS record for the same domain, which is always followed by AUDIT_SYSCALL and AUDIT_PROCTITLE. This is in line with the audit logic to first record the cause of an event, and then add context with other types of record. Audit event sample for a first denial: type=LANDLOCK_ACCESS msg=audit(1732186800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd" type=LANDLOCK_DOMAIN msg=audit(1732186800.349:44): domain=195ba459b status=allocated mode=enforcing pid=300 uid=0 exe="/root/sandboxer" comm="sandboxer" type=SYSCALL msg=audit(1732186800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0 Audit event sample for a following denial: type=LANDLOCK_ACCESS msg=audit(1732186800.372:45): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd" type=SYSCALL msg=audit(1732186800.372:45): arch=c000003e syscall=101 success=no [...] pid=300 auid=0 Log domain deletion with the "deallocated" state when a domain was previously logged. This makes it possible for log parsers to free potential resources when a domain ID will never show again. The number of denied access requests is useful to easily check how many access requests a domain blocked and potentially if some of them are missing in logs because of audit rate limiting or audit rules. Rate limiting could also drop this record though. Audit event sample for a deletion of a domain that denied something: type=LANDLOCK_DOMAIN msg=audit(1732186800.393:46): domain=195ba459b status=deallocated denials=2 Cc: Günther Noack Acked-by: Paul Moore (Audit) Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-11-mic@digikod.net --- Changes since v5: - Add Acked-by Paul. - Improve comment. - Cosmetic code move in landlock_log_denial() for consistency, and to get a cleaner diff regarding the non-changing audit_enabled check. - Remove unlikely(). Changes since v4: - Rename AUDIT_LANDLOCK_DOM_{INFO,DROP} to AUDIT_LANDLOCK_DOMAIN and add a "status" field, as requested by Paul. - Add a harcoded "mode=enforcing" to leave room for a potential future permissive mode, as suggested by Paul. - Remove the "creation" timestamp, as suggested by Paul. - Move LANDLOCK_PATH_MAX_SIZE to domain.h, check the size of the greatest landlock_details at build time, and improve comments. - Improve audit check in landlock_log_drop_domain(). - Add missing headers. - Fix typo in comment. - Rebase on top of the landlock_log_denial() and subject type changes. Changes since v3: - Log number of denied access requests with AUDIT_LANDLOCK_DOM_DROP records, suggested by Tyler. - Do not store a struct path pointer but the resolved string instead. This enables us to not block unmount of the initially restricted task executable's mount point. See the new get_current_info() and get_current_exe(). A following patch add tests for this case. - Create and allocate a new struct landlock_details for initially restricted task's information. - Remove audit_get_ctime() call, as requested by Paul. We now always have a standalone timestamp per Landlock domain creations. - Fix docstring. Changes since v2: - Fix docstring. - Fix log_status check in log_hierarchy() to also log LANDLOCK_LOG_DISABLED. - Add audit's creation time to domain's properties. - Use hexadecimal notation for domain IDs. - Remove domain's parent records: parent domains are not really useful in the logs. They will be available with the upcoming introspection feature though. - Extend commit message with audit's timestamp explanation. Changes since v1: - Add a ruleset's version for atomic logs. - Rebased on the TCP patch series. - Rename operation using "_" instead of "-". - Rename AUDIT_LANDLOCK to AUDIT_LANDLOCK_RULESET. - Only log when audit is enabled, but always set domain IDs. - Don't log task's PID/TID with log_task() because it would be redundant with the SYSCALL record. - Remove race condition when logging ruleset creation and logging ruleset modification while the related file descriptor was already registered but the ruleset creation not logged yet. - Fix domain drop logs. - Move the domain drop record from the previous patch into this one. - Do not log domain creation but log first domain use instead. - Save task's properties that sandbox themselves. --- include/uapi/linux/audit.h | 1 + security/landlock/audit.c | 90 ++++++++++++++++++++++++++++++-- security/landlock/audit.h | 7 +++ security/landlock/domain.c | 101 ++++++++++++++++++++++++++++++++++++ security/landlock/domain.h | 68 ++++++++++++++++++++++++ security/landlock/ruleset.c | 6 +++ 6 files changed, 270 insertions(+), 3 deletions(-) diff --git a/include/uapi/linux/audit.h b/include/uapi/linux/audit.h index 5dd53f416a4a..9a4ecc9f6dc5 100644 --- a/include/uapi/linux/audit.h +++ b/include/uapi/linux/audit.h @@ -147,6 +147,7 @@ #define AUDIT_IPE_CONFIG_CHANGE 1421 /* IPE config change */ #define AUDIT_IPE_POLICY_LOAD 1422 /* IPE policy load */ #define AUDIT_LANDLOCK_ACCESS 1423 /* Landlock denial */ +#define AUDIT_LANDLOCK_DOMAIN 1424 /* Landlock domain status */ #define AUDIT_FIRST_KERN_ANOM_MSG 1700 #define AUDIT_LAST_KERN_ANOM_MSG 1799 diff --git a/security/landlock/audit.c b/security/landlock/audit.c index 8c7c71386a66..507771a6710d 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "audit.h" #include "cred.h" @@ -31,6 +33,40 @@ static void log_blockers(struct audit_buffer *const ab, audit_log_format(ab, "%s", get_blocker(type)); } +static void log_node(struct landlock_hierarchy *const node) +{ + struct audit_buffer *ab; + + if (WARN_ON_ONCE(!node)) + return; + + /* Ignores already logged domains. */ + if (READ_ONCE(node->log_status) == LANDLOCK_LOG_RECORDED) + return; + + ab = audit_log_start(audit_context(), GFP_ATOMIC, + AUDIT_LANDLOCK_DOMAIN); + if (!ab) + return; + + WARN_ON_ONCE(node->id == 0); + audit_log_format( + ab, + "domain=%llx status=allocated mode=enforcing pid=%d uid=%u exe=", + node->id, pid_nr(node->details->pid), + from_kuid(&init_user_ns, node->details->cred->uid)); + audit_log_untrustedstring(ab, node->details->exe_path); + audit_log_format(ab, " comm="); + audit_log_untrustedstring(ab, node->details->comm); + audit_log_end(ab); + + /* + * There may be race condition leading to logging of the same domain + * several times but that is OK. + */ + WRITE_ONCE(node->log_status, LANDLOCK_LOG_RECORDED); +} + static struct landlock_hierarchy * get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer) { @@ -106,12 +142,20 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, if (!is_valid_request(request)) return; - if (!audit_enabled) - return; - youngest_layer = request->layer_plus_one - 1; youngest_denied = get_hierarchy(subject->domain, youngest_layer); + /* + * Consistently keeps track of the number of denied access requests + * even if audit is currently disabled, or if audit rules currently + * exclude this record type, or if landlock_restrict_self(2)'s flags + * quiet logs. + */ + atomic64_inc(&youngest_denied->num_denials); + + if (!audit_enabled) + return; + /* Ignores denials after an execution. */ if (!(subject->domain_exec & (1 << youngest_layer))) return; @@ -125,6 +169,46 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, log_blockers(ab, request->type); audit_log_lsm_data(ab, &request->audit); audit_log_end(ab); + + /* Logs this domain if it is the first time. */ + log_node(youngest_denied); +} + +/** + * landlock_log_drop_domain - Create an audit record when a domain is deleted + * + * @domain: The domain being deleted. + * + * Only domains which previously appeared in the audit logs are logged again. + * This is useful to know when a domain will never show again in the audit log. + * + * This record is not directly tied to a syscall entry. + * + * Called by the cred_free() hook, in an uninterruptible context. + */ +void landlock_log_drop_domain(const struct landlock_ruleset *const domain) +{ + struct audit_buffer *ab; + + if (WARN_ON_ONCE(!domain->hierarchy)) + return; + + if (!audit_enabled) + return; + + /* Ignores domains that were not logged. */ + if (READ_ONCE(domain->hierarchy->log_status) != LANDLOCK_LOG_RECORDED) + return; + + ab = audit_log_start(audit_context(), GFP_ATOMIC, + AUDIT_LANDLOCK_DOMAIN); + if (!ab) + return; + + audit_log_format(ab, "domain=%llx status=deallocated denials=%llu", + domain->hierarchy->id, + atomic64_read(&domain->hierarchy->num_denials)); + audit_log_end(ab); } #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST diff --git a/security/landlock/audit.h b/security/landlock/audit.h index daca14d77649..0608241eb7e1 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -37,11 +37,18 @@ struct landlock_request { #ifdef CONFIG_AUDIT +void landlock_log_drop_domain(const struct landlock_ruleset *const domain); + void landlock_log_denial(const struct landlock_cred_security *const subject, const struct landlock_request *const request); #else /* CONFIG_AUDIT */ +static inline void +landlock_log_drop_domain(const struct landlock_ruleset *const domain) +{ +} + static inline void landlock_log_denial(const struct landlock_cred_security *const subject, const struct landlock_request *const request) diff --git a/security/landlock/domain.c b/security/landlock/domain.c index f6877ae79380..6a731efca7be 100644 --- a/security/landlock/domain.c +++ b/security/landlock/domain.c @@ -7,21 +7,122 @@ * Copyright © 2024-2025 Microsoft Corporation */ +#include +#include +#include +#include +#include +#include + #include "domain.h" +#include "fs.h" #include "id.h" #ifdef CONFIG_AUDIT +/** + * get_current_exe - Get the current's executable path, if any + * + * @exe_str: Returned pointer to a path string with a lifetime tied to the + * returned buffer, if any. + * @exe_size: Returned size of @exe_str (including the trailing null + * character), if any. + * + * Returns: A pointer to an allocated buffer where @exe_str point to, %NULL if + * there is no executable path, or an error otherwise. + */ +static const void *get_current_exe(const char **const exe_str, + size_t *const exe_size) +{ + const size_t buffer_size = LANDLOCK_PATH_MAX_SIZE; + struct mm_struct *mm = current->mm; + struct file *file __free(fput) = NULL; + char *buffer __free(kfree) = NULL; + const char *exe; + size_t size; + + if (!mm) + return NULL; + + file = get_mm_exe_file(mm); + if (!file) + return NULL; + + buffer = kmalloc(buffer_size, GFP_KERNEL); + if (!buffer) + return ERR_PTR(-ENOMEM); + + exe = d_path(&file->f_path, buffer, buffer_size); + if (WARN_ON_ONCE(IS_ERR(exe))) + /* Should never happen according to LANDLOCK_PATH_MAX_SIZE. */ + return ERR_CAST(exe); + + size = buffer + buffer_size - exe; + if (WARN_ON_ONCE(size <= 0)) + return ERR_PTR(-ENAMETOOLONG); + + *exe_size = size; + *exe_str = exe; + return no_free_ptr(buffer); +} + +/* + * Returns: A newly allocated object describing a domain, or an error + * otherwise. + */ +static struct landlock_details *get_current_details(void) +{ + /* Cf. audit_log_d_path_exe() */ + static const char null_path[] = "(null)"; + const char *path_str = null_path; + size_t path_size = sizeof(null_path); + const void *buffer __free(kfree) = NULL; + struct landlock_details *details; + + buffer = get_current_exe(&path_str, &path_size); + if (IS_ERR(buffer)) + return ERR_CAST(buffer); + + /* + * Create the new details according to the path's length. Do not + * allocate with GFP_KERNEL_ACCOUNT because it is independent from the + * caller. + */ + details = + kzalloc(struct_size(details, exe_path, path_size), GFP_KERNEL); + if (!details) + return ERR_PTR(-ENOMEM); + + memcpy(details->exe_path, path_str, path_size); + WARN_ON_ONCE(current_cred() != current_real_cred()); + details->cred = get_current_cred(); + details->pid = get_pid(task_pid(current)); + get_task_comm(details->comm, current); + return details; +} + /** * landlock_init_hierarchy_log - Partially initialize landlock_hierarchy * * @hierarchy: The hierarchy to initialize. * + * The current task is referenced as the domain restrictor. The subjective + * credentials must not be in an overridden state. + * * @hierarchy->parent and @hierarchy->usage should already be set. */ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) { + struct landlock_details *details; + + details = get_current_details(); + if (IS_ERR(details)) + return PTR_ERR(details); + + hierarchy->details = details; hierarchy->id = landlock_get_id_range(1); + hierarchy->log_status = LANDLOCK_LOG_PENDING; + atomic64_set(&hierarchy->num_denials, 0); return 0; } diff --git a/security/landlock/domain.h b/security/landlock/domain.h index 1020878180d3..008ea7a26cb2 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -10,8 +10,61 @@ #ifndef _SECURITY_LANDLOCK_DOMAIN_H #define _SECURITY_LANDLOCK_DOMAIN_H +#include +#include #include +#include +#include #include +#include +#include + +enum landlock_log_status { + LANDLOCK_LOG_PENDING = 0, + LANDLOCK_LOG_RECORDED, +}; + +/** + * struct landlock_details - Domain's creation information + * + * Rarely accessed, mainly when logging the first domain's denial. + * + * The contained pointers are initialized at the domain creation time and never + * changed again. Contrary to most other Landlock object types, this one is + * not allocated with GFP_KERNEL_ACCOUNT because its size may not be under the + * caller's control (e.g. unknown exe_path) and the data is not explicitly + * requested nor used by tasks. + */ +struct landlock_details { + /** + * @cred: Credential of the task that initially restricted itself, at + * creation time. + */ + const struct cred *cred; + /** + * @pid: PID of the task that initially restricted itself. It still + * identifies the same task. + */ + struct pid *pid; + /** + * @comm: Command line of the task that initially restricted itself, at + * creation time. Always NULL terminated. + */ + char comm[TASK_COMM_LEN]; + /** + * @exe_path: Executable path of the task that initially restricted + * itself, at creation time. Always NULL terminated, and never greater + * than LANDLOCK_PATH_MAX_SIZE. + */ + char exe_path[]; +}; + +/* Adds 11 extra characters for the potential " (deleted)" suffix. */ +#define LANDLOCK_PATH_MAX_SIZE (PATH_MAX + 11) + +/* Makes sure the greatest landlock_details can be allocated. */ +static_assert(struct_size_t(struct landlock_details, exe_path, + LANDLOCK_PATH_MAX_SIZE) <= KMALLOC_MAX_SIZE); /** * struct landlock_hierarchy - Node in a domain hierarchy @@ -29,10 +82,25 @@ struct landlock_hierarchy { refcount_t usage; #ifdef CONFIG_AUDIT + /** + * @log_status: Whether this domain should be logged or not. Because + * concurrent log entries may be created at the same time, it is still + * possible to have several domain records of the same domain. + */ + enum landlock_log_status log_status; + /** + * @num_denials: Number of access requests denied by this domain. + * Masked (i.e. never logged) denials are still counted. + */ + atomic64_t num_denials; /** * @id: Landlock domain ID, sets once at domain creation time. */ u64 id; + /** + * @details: Information about the related domain. + */ + const struct landlock_details *details; #endif /* CONFIG_AUDIT */ }; diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index f273a40e9780..724c5efed9b1 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -510,6 +510,9 @@ static void free_ruleset_work(struct work_struct *const work) void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset) { if (ruleset && refcount_dec_and_test(&ruleset->usage)) { + /* Logs with the current context. */ + landlock_log_drop_domain(ruleset); + INIT_WORK(&ruleset->work_free, free_ruleset_work); schedule_work(&ruleset->work_free); } @@ -521,6 +524,9 @@ void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset) * @parent: Parent domain. * @ruleset: New ruleset to be merged. * + * The current task is requesting to be restricted. The subjective credentials + * must not be in an overridden state. cf. landlock_init_hierarchy_log(). + * * Returns the intersection of @parent and @ruleset, or returns @parent if * @ruleset is empty, or returns a duplicate of @ruleset if @parent is empty. */ From patchwork Sat Mar 8 18:44:07 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007677 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-bc08.mail.infomaniak.ch (smtp-bc08.mail.infomaniak.ch [45.157.188.8]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id F04602135AB for ; Sat, 8 Mar 2025 18:44:50 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=45.157.188.8 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459493; cv=none; b=IUdURB0Z/Xs8zfGUyNo+l+IimJBjviAqI/HMXmiGMUv5UedDR6G1BHfuW2jSgQ3QvIBOJLsOgUO/LY9H9iArOyq33iY2b0B97gTxRIDpg8h/eH9NM5/5isnAOg9in4Gke6U+NWcQHMW4sAazy05TrVOLe4mBGlxm5Y1rWluavvY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459493; c=relaxed/simple; bh=JEdzQKgr/LpUrQle/L/hutF1xQQyGDZyxiYb4Q0by38=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=QvmqWGiQdJg63KSVb/J2SslWUKmxt0X+tJ1diZSgjmI7i9u5QTHxKKoFPn6sLWN9mW/QXL1ELUseMTwDhLlfTbKW22Ssd1pWi5un2nZIik3XM7399BqBEHB5X9HUM9RRQTvT+3PMCfGIgA7z/F4c/4XSvFlX9Qz04zYc1xDF4MQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=fHG0V/ld; arc=none smtp.client-ip=45.157.188.8 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="fHG0V/ld" Received: from smtp-3-0000.mail.infomaniak.ch (smtp-3-0000.mail.infomaniak.ch [10.4.36.107]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9BsY2pTlzQwy; Sat, 8 Mar 2025 19:44:49 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459489; bh=UkXnaRvTkVKf8Qk2hG+ipdOUAkahG0b1JanDw+3kvxI=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=fHG0V/ldKZKpgZus0zf/4Rt/cPmLaODV3AWHTnbTqxzxdW2WZBhpFsvybcukzytgz m1YR84KmrC4VmIqIJaMzT4TE0zIrc66My7OsKxyi2jB0ZvpjlTI9PU6t6Mw/LvohtC f/JbUvuXh0NvVLLB9nlen2PcuCu3EmRy+46QSdIM= Received: from unknown by smtp-3-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsX4vxJzsMX; Sat, 8 Mar 2025 19:44:48 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 11/26] landlock: Log mount-related denials Date: Sat, 8 Mar 2025 19:44:07 +0100 Message-ID: <20250308184422.2159360-12-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add audit support for sb_mount, move_mount, sb_umount, sb_remount, and sb_pivot_root hooks. The new related blocker is "fs.change_layout". Audit event sample: type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.change_layout name="/" dev="tmpfs" ino=1 Remove landlock_get_applicable_domain() and get_current_fs_domain() which are now fully replaced with landlock_get_applicable_subject(). Cc: Günther Noack Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-12-mic@digikod.net --- Changes since v5: - Move request declaration in the landlock_log_denial() calls. Changes since v4: - Rebase on top of the landlock_log_denial() and subject type changes. - Fix off-by-one error in landlock_match_layer_level(), now merged into landlock_get_applicable_domain(). Changes since v3: - Cosmetic change to the "fs.change_layout" name. Changes since v2: - Log the domain that denied the action because not all layers block FS layout changes. - Fix landlock_match_layer_level(). Changes since v1: - Rebased on the TCP patch series. - Don't log missing permissions, only domain layer, and then remove the permission word (suggested by Günther) --- security/landlock/audit.c | 3 ++ security/landlock/audit.h | 1 + security/landlock/fs.c | 81 ++++++++++++++++++++++++++++++++----- security/landlock/ruleset.h | 30 -------------- 4 files changed, 74 insertions(+), 41 deletions(-) diff --git a/security/landlock/audit.c b/security/landlock/audit.c index 507771a6710d..f754e4e719fa 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -21,6 +21,9 @@ static const char *get_blocker(const enum landlock_request_type type) switch (type) { case LANDLOCK_REQUEST_PTRACE: return "ptrace"; + + case LANDLOCK_REQUEST_FS_CHANGE_LAYOUT: + return "fs.change_layout"; } WARN_ON_ONCE(1); diff --git a/security/landlock/audit.h b/security/landlock/audit.h index 0608241eb7e1..258b7e3cd9a5 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -16,6 +16,7 @@ enum landlock_request_type { LANDLOCK_REQUEST_PTRACE = 1, + LANDLOCK_REQUEST_FS_CHANGE_LAYOUT, }; /* diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 7dbfc6420e1b..29f964ae4195 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include "access.h" +#include "audit.h" #include "common.h" #include "cred.h" #include "fs.h" @@ -393,12 +395,6 @@ static const struct access_masks any_fs = { .fs = ~0, }; -static const struct landlock_ruleset *get_current_fs_domain(void) -{ - return landlock_get_applicable_domain(landlock_get_current_domain(), - any_fs); -} - /* * Check that a destination file hierarchy has more restrictions than a source * file hierarchy. This is only used for link and rename actions. @@ -1333,6 +1329,34 @@ static void hook_sb_delete(struct super_block *const sb) !atomic_long_read(&landlock_superblock(sb)->inode_refs)); } +static void +log_fs_change_layout_path(const struct landlock_cred_security *const subject, + size_t handle_layer, const struct path *const path) +{ + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_FS_CHANGE_LAYOUT, + .audit = { + .type = LSM_AUDIT_DATA_PATH, + .u.path = *path, + }, + .layer_plus_one = handle_layer + 1, + }); +} + +static void +log_fs_change_layout_dentry(const struct landlock_cred_security *const subject, + size_t handle_layer, struct dentry *const dentry) +{ + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_FS_CHANGE_LAYOUT, + .audit = { + .type = LSM_AUDIT_DATA_DENTRY, + .u.dentry = dentry, + }, + .layer_plus_one = handle_layer + 1, + }); +} + /* * Because a Landlock security policy is defined according to the filesystem * topology (i.e. the mount namespace), changing it may grant access to files @@ -1355,16 +1379,30 @@ static int hook_sb_mount(const char *const dev_name, const struct path *const path, const char *const type, const unsigned long flags, void *const data) { - if (!get_current_fs_domain()) + size_t handle_layer; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, + &handle_layer); + + if (!subject) return 0; + + log_fs_change_layout_path(subject, handle_layer, path); return -EPERM; } static int hook_move_mount(const struct path *const from_path, const struct path *const to_path) { - if (!get_current_fs_domain()) + size_t handle_layer; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, + &handle_layer); + + if (!subject) return 0; + + log_fs_change_layout_path(subject, handle_layer, to_path); return -EPERM; } @@ -1374,15 +1412,29 @@ static int hook_move_mount(const struct path *const from_path, */ static int hook_sb_umount(struct vfsmount *const mnt, const int flags) { - if (!get_current_fs_domain()) + size_t handle_layer; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, + &handle_layer); + + if (!subject) return 0; + + log_fs_change_layout_dentry(subject, handle_layer, mnt->mnt_root); return -EPERM; } static int hook_sb_remount(struct super_block *const sb, void *const mnt_opts) { - if (!get_current_fs_domain()) + size_t handle_layer; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, + &handle_layer); + + if (!subject) return 0; + + log_fs_change_layout_dentry(subject, handle_layer, sb->s_root); return -EPERM; } @@ -1397,8 +1449,15 @@ static int hook_sb_remount(struct super_block *const sb, void *const mnt_opts) static int hook_sb_pivotroot(const struct path *const old_path, const struct path *const new_path) { - if (!get_current_fs_domain()) + size_t handle_layer; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, + &handle_layer); + + if (!subject) return 0; + + log_fs_change_layout_path(subject, handle_layer, new_path); return -EPERM; } diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h index bbb5996545d2..27a4f92ae82c 100644 --- a/security/landlock/ruleset.h +++ b/security/landlock/ruleset.h @@ -242,36 +242,6 @@ landlock_union_access_masks(const struct landlock_ruleset *const domain) return matches.masks; } -/** - * landlock_get_applicable_domain - Return @domain if it applies to (handles) - * at least one of the access rights specified - * in @masks - * - * @domain: Landlock ruleset (used as a domain) - * @masks: access masks - * - * Returns: @domain if any access rights specified in @masks is handled, or - * NULL otherwise. - */ -static inline const struct landlock_ruleset * -landlock_get_applicable_domain(const struct landlock_ruleset *const domain, - const struct access_masks masks) -{ - const union access_masks_all masks_all = { - .masks = masks, - }; - union access_masks_all merge = {}; - - if (!domain) - return NULL; - - merge.masks = landlock_union_access_masks(domain); - if (merge.all & masks_all.all) - return domain; - - return NULL; -} - static inline void landlock_add_fs_access_mask(struct landlock_ruleset *const ruleset, const access_mask_t fs_access_mask, From patchwork Sat Mar 8 18:44:08 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007702 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-bc0b.mail.infomaniak.ch (smtp-bc0b.mail.infomaniak.ch [45.157.188.11]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id ACAC7197A76 for ; Sat, 8 Mar 2025 18:50:11 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=45.157.188.11 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459814; cv=none; b=kQGGRANnb1KZccqHAiHRhrh3wg0KcYVQBtp/c4yyXQZFFKX/hU3TXvvV6LFoasL/RqheRxEgDbld2Dv80Nq+zkg6p4H6bcCtyumTbESqZCSXhs9JjF/if3zyuPob3zoKAmIlR2Ee1CFQGRc5mc7PVE6Mj2R22qpa0QgeKnn7pac= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459814; c=relaxed/simple; bh=5kbeD8gf0OdG134/x0HDfdIUMAAkozPLfwv8AswwuBg=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=Ddt9chuPQ8ymQ2YcVE0/jP9ZcYeuq47Z9Xj7e4inWhqvYK1quJTqZr2FVQt5DxZfZt2o5OXKNAk610YVbkIpxRuGKKqFRMqcNfLJamtGC2tkOhVohff68rd9ZB9ccEmbJuokZWCe3VBshDL1c+BU1beKtv3La7u1DXsATY8aynQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=WDmLRAg3; arc=none smtp.client-ip=45.157.188.11 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="WDmLRAg3" Received: from smtp-4-0000.mail.infomaniak.ch (unknown [IPv6:2001:1600:7:10:40ca:feff:fe05:0]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsb0npgzS6g; Sat, 8 Mar 2025 19:44:51 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459490; bh=/X31lEMwJJaWjw7oGdE9zBzzg58ABkgo598XjaUjD50=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=WDmLRAg3F0gMl6/6pxBW8ralL0xQikufuYm+iamm8aBHdfZziReJliMPO3BH76YRr YN54HgZPKRmECU6zLh5cIvDEe4j3UoPrxoDzY9O9f7jY+VQ3feVlL024dyp3ktqhNs 3A2+dYBNRZxgGY/BrJXTRy0kBXzNNiHO+mQLUPWY= Received: from unknown by smtp-4-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9BsZ14pQzHxP; Sat, 8 Mar 2025 19:44:50 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 12/26] landlock: Log file-related denials Date: Sat, 8 Mar 2025 19:44:08 +0100 Message-ID: <20250308184422.2159360-13-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add audit support for path_mkdir, path_mknod, path_symlink, path_unlink, path_rmdir, path_truncate, path_link, path_rename, and file_open hooks. The dedicated blockers are: - fs.execute - fs.write_file - fs.read_file - fs.read_dir - fs.remove_dir - fs.remove_file - fs.make_char - fs.make_dir - fs.make_reg - fs.make_sock - fs.make_fifo - fs.make_block - fs.make_sym - fs.refer - fs.truncate - fs.ioctl_dev Audit event sample for a denied link action: type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.refer path="/usr/bin" dev="vda2" ino=351 type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.make_reg,fs.refer path="/usr/local" dev="vda2" ino=365 We could pack blocker names (e.g. "fs:make_reg,refer") but that would increase complexity for the kernel and log parsers. Moreover, this could not handle blockers of different classes (e.g. fs and net). Make it simple and flexible instead. Add KUnit tests to check the identification from a layer_mask_t array of the first layer level denying such request. Cc: Günther Noack Depends-on: 058518c20920 ("landlock: Align partial refer access checks with final ones") Depends-on: d617f0d72d80 ("landlock: Optimize file path walks and prepare for audit support") Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-13-mic@digikod.net --- Changes since v5: - Fix log_request_* update typo in is_access_to_paths_allowed(). Changes since v4: - Rebase on top of the landlock_log_denial() and subject type changes. - Add Depends-on tags. Changes since v3: - Rename blockers from fs_* to fs.* - Extend commit message. Changes since v2: - Replace integer with bool in log_blockers(). - Always initialize youngest_layer, spotted by Francis Laniel. - Fix incorrect log reason by using access_masked_parent1 instead of access_request_parent1 (thanks to the previous fix patches). - Clean up formatting. Changes since v1: - Move audit code to the ptrace patch. - Revamp logging and support the path_link and path_rename hooks. - Add KUnit tests. --- security/landlock/audit.c | 178 ++++++++++++++++++++++++++++++++++++-- security/landlock/audit.h | 9 ++ security/landlock/fs.c | 62 +++++++++++-- 3 files changed, 233 insertions(+), 16 deletions(-) diff --git a/security/landlock/audit.c b/security/landlock/audit.c index f754e4e719fa..bda095822aa2 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -7,23 +7,56 @@ #include #include +#include #include #include #include +#include #include "audit.h" +#include "common.h" #include "cred.h" #include "domain.h" #include "ruleset.h" -static const char *get_blocker(const enum landlock_request_type type) +static const char *const fs_access_strings[] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "fs.execute", + [BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = "fs.write_file", + [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = "fs.read_file", + [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = "fs.read_dir", + [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = "fs.remove_dir", + [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_FILE)] = "fs.remove_file", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_CHAR)] = "fs.make_char", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_DIR)] = "fs.make_dir", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_REG)] = "fs.make_reg", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SOCK)] = "fs.make_sock", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_FIFO)] = "fs.make_fifo", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_BLOCK)] = "fs.make_block", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SYM)] = "fs.make_sym", + [BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer", + [BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate", + [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev", +}; + +static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS); + +static __attribute_const__ const char * +get_blocker(const enum landlock_request_type type, + const unsigned long access_bit) { switch (type) { case LANDLOCK_REQUEST_PTRACE: + WARN_ON_ONCE(access_bit != -1); return "ptrace"; case LANDLOCK_REQUEST_FS_CHANGE_LAYOUT: + WARN_ON_ONCE(access_bit != -1); return "fs.change_layout"; + + case LANDLOCK_REQUEST_FS_ACCESS: + if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings))) + return "unknown"; + return fs_access_strings[access_bit]; } WARN_ON_ONCE(1); @@ -31,9 +64,20 @@ static const char *get_blocker(const enum landlock_request_type type) } static void log_blockers(struct audit_buffer *const ab, - const enum landlock_request_type type) + const enum landlock_request_type type, + const access_mask_t access) { - audit_log_format(ab, "%s", get_blocker(type)); + const unsigned long access_mask = access; + unsigned long access_bit; + bool is_first = true; + + for_each_set_bit(access_bit, &access_mask, BITS_PER_TYPE(access)) { + audit_log_format(ab, "%s%s", is_first ? "" : ",", + get_blocker(type, access_bit)); + is_first = false; + } + if (is_first) + audit_log_format(ab, "%s", get_blocker(type, -1)); } static void log_node(struct landlock_hierarchy *const node) @@ -117,9 +161,110 @@ static void test_get_hierarchy(struct kunit *const test) #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ +static size_t get_denied_layer(const struct landlock_ruleset *const domain, + access_mask_t *const access_request, + const layer_mask_t (*const layer_masks)[], + const size_t layer_masks_size) +{ + const unsigned long access_req = *access_request; + unsigned long access_bit; + access_mask_t missing = 0; + long youngest_layer = -1; + + for_each_set_bit(access_bit, &access_req, layer_masks_size) { + const access_mask_t mask = (*layer_masks)[access_bit]; + long layer; + + if (!mask) + continue; + + /* __fls(1) == 0 */ + layer = __fls(mask); + if (layer > youngest_layer) { + youngest_layer = layer; + missing = BIT(access_bit); + } else if (layer == youngest_layer) { + missing |= BIT(access_bit); + } + } + + *access_request = missing; + if (youngest_layer == -1) + return domain->num_layers - 1; + + return youngest_layer; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_get_denied_layer(struct kunit *const test) +{ + const struct landlock_ruleset dom = { + .num_layers = 5, + }; + const layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT(0), + [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = BIT(1), + [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = BIT(1) | BIT(0), + [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = BIT(2), + }; + access_mask_t access; + + access = LANDLOCK_ACCESS_FS_EXECUTE; + KUNIT_EXPECT_EQ(test, 0, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_EXECUTE); + + access = LANDLOCK_ACCESS_FS_READ_FILE; + KUNIT_EXPECT_EQ(test, 1, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_FILE); + + access = LANDLOCK_ACCESS_FS_READ_DIR; + KUNIT_EXPECT_EQ(test, 1, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR); + + access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR; + KUNIT_EXPECT_EQ(test, 1, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR); + + access = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR; + KUNIT_EXPECT_EQ(test, 1, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR); + + access = LANDLOCK_ACCESS_FS_WRITE_FILE; + KUNIT_EXPECT_EQ(test, 4, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, 0); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + static bool is_valid_request(const struct landlock_request *const request) { - if (WARN_ON_ONCE(!request->layer_plus_one)) + if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access))) + return false; + + if (request->access) { + if (WARN_ON_ONCE(!request->layer_masks)) + return false; + } else { + if (WARN_ON_ONCE(request->layer_masks)) + return false; + } + + if (WARN_ON_ONCE(!!request->layer_masks ^ !!request->layer_masks_size)) return false; return true; @@ -137,6 +282,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, struct audit_buffer *ab; struct landlock_hierarchy *youngest_denied; size_t youngest_layer; + access_mask_t missing; if (WARN_ON_ONCE(!subject || !subject->domain || !subject->domain->hierarchy || !request)) @@ -145,8 +291,25 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, if (!is_valid_request(request)) return; - youngest_layer = request->layer_plus_one - 1; - youngest_denied = get_hierarchy(subject->domain, youngest_layer); + missing = request->access; + if (missing) { + /* Gets the nearest domain that denies the request. */ + if (request->layer_masks) { + youngest_layer = get_denied_layer( + subject->domain, &missing, request->layer_masks, + request->layer_masks_size); + } else { + /* This will change with the next commit. */ + WARN_ON_ONCE(1); + youngest_layer = subject->domain->num_layers; + } + youngest_denied = + get_hierarchy(subject->domain, youngest_layer); + } else { + youngest_layer = request->layer_plus_one - 1; + youngest_denied = + get_hierarchy(subject->domain, youngest_layer); + } /* * Consistently keeps track of the number of denied access requests @@ -169,7 +332,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, return; audit_log_format(ab, "domain=%llx blockers=", youngest_denied->id); - log_blockers(ab, request->type); + log_blockers(ab, request->type, missing); audit_log_lsm_data(ab, &request->audit); audit_log_end(ab); @@ -219,6 +382,7 @@ void landlock_log_drop_domain(const struct landlock_ruleset *const domain) static struct kunit_case test_cases[] = { /* clang-format off */ KUNIT_CASE(test_get_hierarchy), + KUNIT_CASE(test_get_denied_layer), {} /* clang-format on */ }; diff --git a/security/landlock/audit.h b/security/landlock/audit.h index 258b7e3cd9a5..d14b779a2b00 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -11,12 +11,14 @@ #include #include +#include "access.h" #include "cred.h" #include "ruleset.h" enum landlock_request_type { LANDLOCK_REQUEST_PTRACE = 1, LANDLOCK_REQUEST_FS_CHANGE_LAYOUT, + LANDLOCK_REQUEST_FS_ACCESS, }; /* @@ -34,6 +36,13 @@ struct landlock_request { * extra one is useful to detect uninitialized field. */ size_t layer_plus_one; + + /* Required field for configurable access control. */ + access_mask_t access; + + /* Required fields for requests with layer masks. */ + const layer_mask_t (*layer_masks)[]; + size_t layer_masks_size; }; #ifdef CONFIG_AUDIT diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 29f964ae4195..ee233128759d 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -724,6 +724,7 @@ static void test_is_eacces_with_write(struct kunit *const test) * those identified by @access_request_parent1). This matrix can * initially refer to domain layer masks and, when the accesses for the * destination and source are the same, to requested layer masks. + * @log_request_parent1: Audit request to fill if the related access is denied. * @dentry_child1: Dentry to the initial child of the parent1 path. This * pointer must be NULL for non-refer actions (i.e. not link nor rename). * @access_request_parent2: Similar to @access_request_parent1 but for a @@ -732,6 +733,7 @@ static void test_is_eacces_with_write(struct kunit *const test) * the source. Must be set to 0 when using a simple path request. * @layer_masks_parent2: Similar to @layer_masks_parent1 but for a refer * action. This must be NULL otherwise. + * @log_request_parent2: Audit request to fill if the related access is denied. * @dentry_child2: Dentry to the initial child of the parent2 path. This * pointer is only set for RENAME_EXCHANGE actions and must be NULL * otherwise. @@ -751,10 +753,12 @@ static bool is_access_to_paths_allowed( const struct path *const path, const access_mask_t access_request_parent1, layer_mask_t (*const layer_masks_parent1)[LANDLOCK_NUM_ACCESS_FS], - const struct dentry *const dentry_child1, + struct landlock_request *const log_request_parent1, + struct dentry *const dentry_child1, const access_mask_t access_request_parent2, layer_mask_t (*const layer_masks_parent2)[LANDLOCK_NUM_ACCESS_FS], - const struct dentry *const dentry_child2) + struct landlock_request *const log_request_parent2, + struct dentry *const dentry_child2) { bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check, child1_is_directory = true, child2_is_directory = true; @@ -919,6 +923,25 @@ static bool is_access_to_paths_allowed( } path_put(&walker_path); + if (!allowed_parent1) { + log_request_parent1->type = LANDLOCK_REQUEST_FS_ACCESS; + log_request_parent1->audit.type = LSM_AUDIT_DATA_PATH; + log_request_parent1->audit.u.path = *path; + log_request_parent1->access = access_masked_parent1; + log_request_parent1->layer_masks = layer_masks_parent1; + log_request_parent1->layer_masks_size = + ARRAY_SIZE(*layer_masks_parent1); + } + + if (!allowed_parent2) { + log_request_parent2->type = LANDLOCK_REQUEST_FS_ACCESS; + log_request_parent2->audit.type = LSM_AUDIT_DATA_PATH; + log_request_parent2->audit.u.path = *path; + log_request_parent2->access = access_masked_parent2; + log_request_parent2->layer_masks = layer_masks_parent2; + log_request_parent2->layer_masks_size = + ARRAY_SIZE(*layer_masks_parent2); + } return allowed_parent1 && allowed_parent2; } @@ -931,6 +954,7 @@ static int current_check_access_path(const struct path *const path, const struct landlock_cred_security *const subject = landlock_get_applicable_subject(current_cred(), masks, NULL); layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {}; + struct landlock_request request = {}; if (!subject) return 0; @@ -939,9 +963,11 @@ static int current_check_access_path(const struct path *const path, access_request, &layer_masks, LANDLOCK_KEY_INODE); if (is_access_to_paths_allowed(subject->domain, path, access_request, - &layer_masks, NULL, 0, NULL, NULL)) + &layer_masks, &request, NULL, 0, NULL, + NULL, NULL)) return 0; + landlock_log_denial(subject, &request); return -EACCES; } @@ -1110,6 +1136,7 @@ static int current_check_refer_path(struct dentry *const old_dentry, struct dentry *old_parent; layer_mask_t layer_masks_parent1[LANDLOCK_NUM_ACCESS_FS] = {}, layer_masks_parent2[LANDLOCK_NUM_ACCESS_FS] = {}; + struct landlock_request request1 = {}, request2 = {}; if (!subject) return 0; @@ -1141,10 +1168,13 @@ static int current_check_refer_path(struct dentry *const old_dentry, subject->domain, access_request_parent1 | access_request_parent2, &layer_masks_parent1, LANDLOCK_KEY_INODE); - if (is_access_to_paths_allowed( - subject->domain, new_dir, access_request_parent1, - &layer_masks_parent1, NULL, 0, NULL, NULL)) + if (is_access_to_paths_allowed(subject->domain, new_dir, + access_request_parent1, + &layer_masks_parent1, &request1, + NULL, 0, NULL, NULL, NULL)) return 0; + + landlock_log_denial(subject, &request1); return -EACCES; } @@ -1183,10 +1213,20 @@ static int current_check_refer_path(struct dentry *const old_dentry, */ if (is_access_to_paths_allowed( subject->domain, &mnt_dir, access_request_parent1, - &layer_masks_parent1, old_dentry, access_request_parent2, - &layer_masks_parent2, exchange ? new_dentry : NULL)) + &layer_masks_parent1, &request1, old_dentry, + access_request_parent2, &layer_masks_parent2, &request2, + exchange ? new_dentry : NULL)) return 0; + if (request1.access) { + request1.audit.u.path.dentry = old_parent; + landlock_log_denial(subject, &request1); + } + if (request2.access) { + request2.audit.u.path.dentry = new_dir->dentry; + landlock_log_denial(subject, &request2); + } + /* * This prioritizes EACCES over EXDEV for all actions, including * renames with RENAME_EXCHANGE. @@ -1576,6 +1616,7 @@ static int hook_file_open(struct file *const file) optional_access; const struct landlock_cred_security *const subject = landlock_get_applicable_subject(file->f_cred, any_fs, NULL); + struct landlock_request request = {}; if (!subject) return 0; @@ -1602,7 +1643,7 @@ static int hook_file_open(struct file *const file) landlock_init_layer_masks(subject->domain, full_access_request, &layer_masks, LANDLOCK_KEY_INODE), - &layer_masks, NULL, 0, NULL, NULL)) { + &layer_masks, &request, NULL, 0, NULL, NULL, NULL)) { allowed_access = full_access_request; } else { unsigned long access_bit; @@ -1632,6 +1673,9 @@ static int hook_file_open(struct file *const file) if ((open_access_request & allowed_access) == open_access_request) return 0; + /* Sets access to reflect the actual request. */ + request.access = open_access_request; + landlock_log_denial(subject, &request); return -EACCES; } From patchwork Sat Mar 8 18:44:09 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007679 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-bc08.mail.infomaniak.ch (smtp-bc08.mail.infomaniak.ch [45.157.188.8]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 7644B214804 for ; Sat, 8 Mar 2025 18:44:54 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=45.157.188.8 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459497; cv=none; b=M2SXG4xIqgnCTVdLqmlR8qehT3sZbiGuivJ9IoA7k+Z8wJv3saAKYMRebyIwRhzaxSyTE46YvfxahXFlsrpncwUSJ2bqEL56lStTydE0JCXJX4rCmRtkZg4DDfpPKcvyvOx/6UrZFnLu+nOYvuMssqKwj50hAJHLDKzHpT/QDzg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459497; c=relaxed/simple; bh=oFxMeXnqX2G1gOHOziBdMSA/w6a02qM1v9C8NYzWddk=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=thayfbTUne9n/VPuGfIBCpl6FqYM8zdhYZ9iTnq3YQ/n1vKct2i7u4DRorpQpUVj1Iu1qNodhS78v4FGMM4DiqKrTGVTIrpRTsXpWP2tVrhIxiRh0rBSmLIs4Ep/yQTdsB4ulqkQXGcY2isfv29mvTn5GtYKLDJwrIFebwokwh8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=BLheSZPn; arc=none smtp.client-ip=45.157.188.8 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="BLheSZPn" Received: from smtp-3-0001.mail.infomaniak.ch (unknown [IPv6:2001:1600:4:17::246c]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsc43cwzMDG; Sat, 8 Mar 2025 19:44:52 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459492; bh=kr7M1k4+/TJxg3zCviSq2t1LZ/ISoxZbqckczhfqc08=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=BLheSZPnKxfNp0/D2D4Z0C5plBIiRz85MPGRLSVLWLRX6sINXO4OjZQ1HK+Ca3flJ SoctsboQGi9Z4abe9v90+NuNaECFCvSnFAL0xe4OKNS5LPLhUSu8mPNlh8iQXsRZaj /WJP7YeJLwsjCxURwJpUWQLkVAoktUtVNHNHGLH8= Received: from unknown by smtp-3-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsb5jKfzW8; Sat, 8 Mar 2025 19:44:51 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 13/26] landlock: Log truncate and IOCTL denials Date: Sat, 8 Mar 2025 19:44:09 +0100 Message-ID: <20250308184422.2159360-14-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add audit support to the file_truncate and file_ioctl hooks. Add a deny_masks_t type and related helpers to store the domain's layer level per optional access rights (i.e. LANDLOCK_ACCESS_FS_TRUNCATE and LANDLOCK_ACCESS_FS_IOCTL_DEV) when opening a file, which cannot be inferred later. In practice, the landlock_file_security aligned blob size is still 16 bytes because this new one-byte deny_masks field follows the existing two-bytes allowed_access field and precede the packed fown_subject. Implementing deny_masks_t with a bitfield instead of a struct enables a generic implementation to store and extract layer levels. Add KUnit tests to check the identification of a layer level from a deny_masks_t, and the computation of a deny_masks_t from an access right with its layer level or a layer_mask_t array. Audit event sample: type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.ioctl_dev path="/dev/tty" dev="devtmpfs" ino=9 ioctlcmd=0x5401 Cc: Günther Noack Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-14-mic@digikod.net --- Changes since v5: - Switch to LSM_AUDIT_DATA_IOCTL_OP for IOCTL hooks, and update sample accordingly. - Move request declarations in the landlock_log_denial() calls to not impact allowed requests with audit, and remove update_request() which is now useless. Changes since v4: - Rebase on top of the landlock_log_denial() and subject type changes. Changes since v3: - Rename get_layer_from_deny_masks(). Changes since v2: - Fix !CONFIG_AUDIT build warning. - Rename ACCESS_FS_OPTIONAL to _LANDLOCK_ACCESS_FS_OPTIONAL. --- security/landlock/access.h | 23 +++++++ security/landlock/audit.c | 102 ++++++++++++++++++++++++++-- security/landlock/audit.h | 4 ++ security/landlock/domain.c | 133 +++++++++++++++++++++++++++++++++++++ security/landlock/domain.h | 8 +++ security/landlock/fs.c | 48 +++++++++++++ security/landlock/fs.h | 9 +++ 7 files changed, 322 insertions(+), 5 deletions(-) diff --git a/security/landlock/access.h b/security/landlock/access.h index 74fd8f399fbd..1eaaafa63178 100644 --- a/security/landlock/access.h +++ b/security/landlock/access.h @@ -28,6 +28,12 @@ LANDLOCK_ACCESS_FS_REFER) /* clang-format on */ +/* clang-format off */ +#define _LANDLOCK_ACCESS_FS_OPTIONAL ( \ + LANDLOCK_ACCESS_FS_TRUNCATE | \ + LANDLOCK_ACCESS_FS_IOCTL_DEV) +/* clang-format on */ + typedef u16 access_mask_t; /* Makes sure all filesystem access rights can be stored. */ @@ -60,6 +66,23 @@ typedef u16 layer_mask_t; /* Makes sure all layers can be checked. */ static_assert(BITS_PER_TYPE(layer_mask_t) >= LANDLOCK_MAX_NUM_LAYERS); +/* + * Tracks domains responsible of a denied access. This is required to avoid + * storing in each object the full layer_masks[] required by update_request(). + */ +typedef u8 deny_masks_t; + +/* + * Makes sure all optional access rights can be tied to a layer index (cf. + * get_deny_mask). + */ +static_assert(BITS_PER_TYPE(deny_masks_t) >= + (HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1) * + HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL))); + +/* LANDLOCK_MAX_NUM_LAYERS must be a power of two (cf. deny_masks_t assert). */ +static_assert(HWEIGHT(LANDLOCK_MAX_NUM_LAYERS) == 1); + /* Upgrades with all initially denied by default access rights. */ static inline struct access_masks landlock_upgrade_handled_access_masks(struct access_masks access_masks) diff --git a/security/landlock/audit.c b/security/landlock/audit.c index bda095822aa2..edb9fbafbcad 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -13,10 +13,12 @@ #include #include +#include "access.h" #include "audit.h" #include "common.h" #include "cred.h" #include "domain.h" +#include "fs.h" #include "ruleset.h" static const char *const fs_access_strings[] = { @@ -251,22 +253,111 @@ static void test_get_denied_layer(struct kunit *const test) #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ +static size_t +get_layer_from_deny_masks(access_mask_t *const access_request, + const access_mask_t all_existing_optional_access, + const deny_masks_t deny_masks) +{ + const unsigned long access_opt = all_existing_optional_access; + const unsigned long access_req = *access_request; + access_mask_t missing = 0; + size_t youngest_layer = 0; + size_t access_index = 0; + unsigned long access_bit; + + /* This will require change with new object types. */ + WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL); + + for_each_set_bit(access_bit, &access_opt, + BITS_PER_TYPE(access_mask_t)) { + if (access_req & BIT(access_bit)) { + const size_t layer = + (deny_masks >> (access_index * 4)) & + (LANDLOCK_MAX_NUM_LAYERS - 1); + + if (layer > youngest_layer) { + youngest_layer = layer; + missing = BIT(access_bit); + } else if (layer == youngest_layer) { + missing |= BIT(access_bit); + } + } + access_index++; + } + + *access_request = missing; + return youngest_layer; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_get_layer_from_deny_masks(struct kunit *const test) +{ + deny_masks_t deny_mask; + access_mask_t access; + + /* truncate:0 ioctl_dev:2 */ + deny_mask = 0x20; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 0, + get_layer_from_deny_masks(&access, + _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks(&access, + _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + + /* truncate:15 ioctl_dev:15 */ + deny_mask = 0xff; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 15, + get_layer_from_deny_masks(&access, + _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 15, + get_layer_from_deny_masks(&access, + _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask)); + KUNIT_EXPECT_EQ(test, access, + LANDLOCK_ACCESS_FS_TRUNCATE | + LANDLOCK_ACCESS_FS_IOCTL_DEV); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + static bool is_valid_request(const struct landlock_request *const request) { if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access))) return false; if (request->access) { - if (WARN_ON_ONCE(!request->layer_masks)) + if (WARN_ON_ONCE(!(!!request->layer_masks ^ + !!request->all_existing_optional_access))) return false; } else { - if (WARN_ON_ONCE(request->layer_masks)) + if (WARN_ON_ONCE(request->layer_masks || + request->all_existing_optional_access)) return false; } if (WARN_ON_ONCE(!!request->layer_masks ^ !!request->layer_masks_size)) return false; + if (request->deny_masks) { + if (WARN_ON_ONCE(!request->all_existing_optional_access)) + return false; + } + return true; } @@ -299,9 +390,9 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, subject->domain, &missing, request->layer_masks, request->layer_masks_size); } else { - /* This will change with the next commit. */ - WARN_ON_ONCE(1); - youngest_layer = subject->domain->num_layers; + youngest_layer = get_layer_from_deny_masks( + &missing, request->all_existing_optional_access, + request->deny_masks); } youngest_denied = get_hierarchy(subject->domain, youngest_layer); @@ -383,6 +474,7 @@ static struct kunit_case test_cases[] = { /* clang-format off */ KUNIT_CASE(test_get_hierarchy), KUNIT_CASE(test_get_denied_layer), + KUNIT_CASE(test_get_layer_from_deny_masks), {} /* clang-format on */ }; diff --git a/security/landlock/audit.h b/security/landlock/audit.h index d14b779a2b00..6765a419001d 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -43,6 +43,10 @@ struct landlock_request { /* Required fields for requests with layer masks. */ const layer_mask_t (*layer_masks)[]; size_t layer_masks_size; + + /* Required fields for requests with deny masks. */ + const access_mask_t all_existing_optional_access; + deny_masks_t deny_masks; }; #ifdef CONFIG_AUDIT diff --git a/security/landlock/domain.c b/security/landlock/domain.c index 6a731efca7be..6704e9283206 100644 --- a/security/landlock/domain.c +++ b/security/landlock/domain.c @@ -7,6 +7,9 @@ * Copyright © 2024-2025 Microsoft Corporation */ +#include +#include +#include #include #include #include @@ -14,6 +17,8 @@ #include #include +#include "access.h" +#include "common.h" #include "domain.h" #include "fs.h" #include "id.h" @@ -126,4 +131,132 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) return 0; } +static deny_masks_t +get_layer_deny_mask(const access_mask_t all_existing_optional_access, + const unsigned long access_bit, const size_t layer) +{ + unsigned long access_weight; + + /* This may require change with new object types. */ + WARN_ON_ONCE(all_existing_optional_access != + _LANDLOCK_ACCESS_FS_OPTIONAL); + + if (WARN_ON_ONCE(layer >= LANDLOCK_MAX_NUM_LAYERS)) + return 0; + + access_weight = hweight_long(all_existing_optional_access & + GENMASK(access_bit, 0)); + if (WARN_ON_ONCE(access_weight < 1)) + return 0; + + return layer + << ((access_weight - 1) * HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1)); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_get_layer_deny_mask(struct kunit *const test) +{ + const unsigned long truncate = BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE); + const unsigned long ioctl_dev = BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV); + + KUNIT_EXPECT_EQ(test, 0, + get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, + truncate, 0)); + KUNIT_EXPECT_EQ(test, 0x3, + get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, + truncate, 3)); + + KUNIT_EXPECT_EQ(test, 0, + get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, + ioctl_dev, 0)); + KUNIT_EXPECT_EQ(test, 0xf0, + get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, + ioctl_dev, 15)); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +deny_masks_t +landlock_get_deny_masks(const access_mask_t all_existing_optional_access, + const access_mask_t optional_access, + const layer_mask_t (*const layer_masks)[], + const size_t layer_masks_size) +{ + const unsigned long access_opt = optional_access; + unsigned long access_bit; + deny_masks_t deny_masks = 0; + + /* This may require change with new object types. */ + WARN_ON_ONCE(access_opt != + (optional_access & all_existing_optional_access)); + + if (WARN_ON_ONCE(!layer_masks)) + return 0; + + if (WARN_ON_ONCE(!access_opt)) + return 0; + + for_each_set_bit(access_bit, &access_opt, layer_masks_size) { + const layer_mask_t mask = (*layer_masks)[access_bit]; + + if (!mask) + continue; + + /* __fls(1) == 0 */ + deny_masks |= get_layer_deny_mask(all_existing_optional_access, + access_bit, __fls(mask)); + } + return deny_masks; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_landlock_get_deny_masks(struct kunit *const test) +{ + const layer_mask_t layers1[BITS_PER_TYPE(access_mask_t)] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0) | + BIT_ULL(9), + [BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = BIT_ULL(1), + [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = BIT_ULL(2) | + BIT_ULL(0), + }; + + KUNIT_EXPECT_EQ(test, 0x1, + landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, + LANDLOCK_ACCESS_FS_TRUNCATE, + &layers1, ARRAY_SIZE(layers1))); + KUNIT_EXPECT_EQ(test, 0x20, + landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, + LANDLOCK_ACCESS_FS_IOCTL_DEV, + &layers1, ARRAY_SIZE(layers1))); + KUNIT_EXPECT_EQ( + test, 0x21, + landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, + LANDLOCK_ACCESS_FS_TRUNCATE | + LANDLOCK_ACCESS_FS_IOCTL_DEV, + &layers1, ARRAY_SIZE(layers1))); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static struct kunit_case test_cases[] = { + /* clang-format off */ + KUNIT_CASE(test_get_layer_deny_mask), + KUNIT_CASE(test_landlock_get_deny_masks), + {} + /* clang-format on */ +}; + +static struct kunit_suite test_suite = { + .name = "landlock_domain", + .test_cases = test_cases, +}; + +kunit_test_suite(test_suite); + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + #endif /* CONFIG_AUDIT */ diff --git a/security/landlock/domain.h b/security/landlock/domain.h index 008ea7a26cb2..c1ab2fe1d441 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -19,6 +19,8 @@ #include #include +#include "access.h" + enum landlock_log_status { LANDLOCK_LOG_PENDING = 0, LANDLOCK_LOG_RECORDED, @@ -125,6 +127,12 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy) int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy); +deny_masks_t +landlock_get_deny_masks(const access_mask_t all_existing_optional_access, + const access_mask_t optional_access, + const layer_mask_t (*const layer_masks)[], + size_t layer_masks_size); + #else /* CONFIG_AUDIT */ static inline int diff --git a/security/landlock/fs.c b/security/landlock/fs.c index ee233128759d..14f9c6d9903f 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1669,6 +1669,11 @@ static int hook_file_open(struct file *const file) * file access rights in the opened struct file. */ landlock_file(file)->allowed_access = allowed_access; +#ifdef CONFIG_AUDIT + landlock_file(file)->deny_masks = landlock_get_deny_masks( + _LANDLOCK_ACCESS_FS_OPTIONAL, optional_access, &layer_masks, + ARRAY_SIZE(layer_masks)); +#endif /* CONFIG_AUDIT */ if ((open_access_request & allowed_access) == open_access_request) return 0; @@ -1693,6 +1698,19 @@ static int hook_file_truncate(struct file *const file) */ if (landlock_file(file)->allowed_access & LANDLOCK_ACCESS_FS_TRUNCATE) return 0; + + landlock_log_denial(landlock_cred(file->f_cred), &(struct landlock_request) { + .type = LANDLOCK_REQUEST_FS_ACCESS, + .audit = { + .type = LSM_AUDIT_DATA_FILE, + .u.file = file, + }, + .all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL, + .access = LANDLOCK_ACCESS_FS_TRUNCATE, +#ifdef CONFIG_AUDIT + .deny_masks = landlock_file(file)->deny_masks, +#endif /* CONFIG_AUDIT */ + }); return -EACCES; } @@ -1716,6 +1734,21 @@ static int hook_file_ioctl(struct file *file, unsigned int cmd, if (is_masked_device_ioctl(cmd)) return 0; + landlock_log_denial(landlock_cred(file->f_cred), &(struct landlock_request) { + .type = LANDLOCK_REQUEST_FS_ACCESS, + .audit = { + .type = LSM_AUDIT_DATA_IOCTL_OP, + .u.op = &(struct lsm_ioctlop_audit) { + .path = file->f_path, + .cmd = cmd, + }, + }, + .all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL, + .access = LANDLOCK_ACCESS_FS_IOCTL_DEV, +#ifdef CONFIG_AUDIT + .deny_masks = landlock_file(file)->deny_masks, +#endif /* CONFIG_AUDIT */ + }); return -EACCES; } @@ -1739,6 +1772,21 @@ static int hook_file_ioctl_compat(struct file *file, unsigned int cmd, if (is_masked_device_ioctl_compat(cmd)) return 0; + landlock_log_denial(landlock_cred(file->f_cred), &(struct landlock_request) { + .type = LANDLOCK_REQUEST_FS_ACCESS, + .audit = { + .type = LSM_AUDIT_DATA_IOCTL_OP, + .u.op = &(struct lsm_ioctlop_audit) { + .path = file->f_path, + .cmd = cmd, + }, + }, + .all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL, + .access = LANDLOCK_ACCESS_FS_IOCTL_DEV, +#ifdef CONFIG_AUDIT + .deny_masks = landlock_file(file)->deny_masks, +#endif /* CONFIG_AUDIT */ + }); return -EACCES; } diff --git a/security/landlock/fs.h b/security/landlock/fs.h index 1449a90e92c7..3a09ba985b74 100644 --- a/security/landlock/fs.h +++ b/security/landlock/fs.h @@ -54,6 +54,15 @@ struct landlock_file_security { * needed to authorize later operations on the open file. */ access_mask_t allowed_access; + +#ifdef CONFIG_AUDIT + /** + * @deny_masks: Domain layer levels that deny an optional access (see + * _LANDLOCK_ACCESS_FS_OPTIONAL). + */ + deny_masks_t deny_masks; +#endif /* CONFIG_AUDIT */ + /** * @fown_subject: Landlock credential of the task that set the PID that * may receive a signal e.g., SIGURG when writing MSG_OOB to the From patchwork Sat Mar 8 18:44:10 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007682 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-190b.mail.infomaniak.ch (smtp-190b.mail.infomaniak.ch [185.125.25.11]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 51745215171 for ; Sat, 8 Mar 2025 18:45:01 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.125.25.11 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459504; cv=none; b=E/v/mqNDVGgFPMrZXsFD/IgHsnMmHXVhlOqJmP54U6IifZncLwxs7wUaK8RzeGReXvb1i0wBerhsdNrvfh3XBgPCotb6ggrOH86f4yY1TRdrijsw+KO1ws0z+k67H+xSKKVm3va38uK34UW3CyeOX3MQFdwChL2fA2kRwfJVNXs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459504; c=relaxed/simple; bh=Wx/6Hx+WDmJ7YB3tMN0nMjUM1VSJv9z1mb1C6KdOc2Q=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=PH6otuMaxu87CyLCSZ2Q+HN5g6aYf9am5uOCPNzVMHgjSjjTuU4zZeGwDcDHrAikbtL/GkLxfPNrM7HYk7kqUAq1zCqmudMvoODVOXg4ZmU6M1zLPpVnFa4uw68ne6q2ND905jM8g25t5UyNoIgTU4hX1QZH4WmyLLWJ4yqfFMU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=x207Suc9; arc=none smtp.client-ip=185.125.25.11 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="x207Suc9" Received: from smtp-4-0000.mail.infomaniak.ch (unknown [IPv6:2001:1600:7:10:40ca:feff:fe05:0]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsf1Nw3zS6s; Sat, 8 Mar 2025 19:44:54 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459494; bh=IYGEJlWXKfihUib9yAQfNAkO9iNl2ovlfA2BSTm6VT8=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=x207Suc9QJufPETUpNZ1Bec+NEpYpsrzJFqecAycdkCakiDOX2dsQLZ+TNKOnk5fE iHAhjsNvL0G7VcjpDU6vKiFIBWqeImFfYkystPRHHh8Pkjz8nsCnu0waz7RPLOrM3E 1IdFp2YFkD3G9zWXwhvXco7evM57rGr/aPp76e00= Received: from unknown by smtp-4-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsd2H7hzHcY; Sat, 8 Mar 2025 19:44:53 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 14/26] landlock: Log TCP bind and connect denials Date: Sat, 8 Mar 2025 19:44:10 +0100 Message-ID: <20250308184422.2159360-15-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add audit support to socket_bind and socket_connect hooks. The related blockers are: - net.bind_tcp - net.connect_tcp Audit event sample: type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=net.connect_tcp daddr=127.0.0.1 dest=80 Cc: Günther Noack Cc: Konstantin Meskhidze Cc: Mikhail Ivanov Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-15-mic@digikod.net --- Changes since v5: - Move request declaration in the landlock_log_denial() call to not impact allowed requests with audit. Changes since v4: - Rebase on top of the landlock_log_denial() and subject type changes. Changes since v3: - Rename blockers from net_* to net.* Changes since v2: - Remove potentially superfluous IPv6 saddr log, spotted by Francis Laniel. - Cosmetic improvements. --- security/landlock/audit.c | 12 +++++++++ security/landlock/audit.h | 1 + security/landlock/net.c | 51 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/security/landlock/audit.c b/security/landlock/audit.c index edb9fbafbcad..6c34758b9ff2 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -42,6 +42,13 @@ static const char *const fs_access_strings[] = { static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS); +static const char *const net_access_strings[] = { + [BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_TCP)] = "net.bind_tcp", + [BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_TCP)] = "net.connect_tcp", +}; + +static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET); + static __attribute_const__ const char * get_blocker(const enum landlock_request_type type, const unsigned long access_bit) @@ -59,6 +66,11 @@ get_blocker(const enum landlock_request_type type, if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings))) return "unknown"; return fs_access_strings[access_bit]; + + case LANDLOCK_REQUEST_NET_ACCESS: + if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(net_access_strings))) + return "unknown"; + return net_access_strings[access_bit]; } WARN_ON_ONCE(1); diff --git a/security/landlock/audit.h b/security/landlock/audit.h index 6765a419001d..eeff2c5bfa4f 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -19,6 +19,7 @@ enum landlock_request_type { LANDLOCK_REQUEST_PTRACE = 1, LANDLOCK_REQUEST_FS_CHANGE_LAYOUT, LANDLOCK_REQUEST_FS_ACCESS, + LANDLOCK_REQUEST_NET_ACCESS, }; /* diff --git a/security/landlock/net.c b/security/landlock/net.c index 6fb3e60bc5ff..f4478e6a2c0b 100644 --- a/security/landlock/net.c +++ b/security/landlock/net.c @@ -7,10 +7,12 @@ */ #include +#include #include #include #include +#include "audit.h" #include "common.h" #include "cred.h" #include "limits.h" @@ -55,6 +57,7 @@ static int current_check_access_socket(struct socket *const sock, }; const struct landlock_cred_security *const subject = landlock_get_applicable_subject(current_cred(), masks, NULL); + struct lsm_network_audit audit_net = {}; if (!subject) return 0; @@ -68,18 +71,48 @@ static int current_check_access_socket(struct socket *const sock, switch (address->sa_family) { case AF_UNSPEC: - case AF_INET: + case AF_INET: { + const struct sockaddr_in *addr4; + if (addrlen < sizeof(struct sockaddr_in)) return -EINVAL; - port = ((struct sockaddr_in *)address)->sin_port; + + addr4 = (struct sockaddr_in *)address; + port = addr4->sin_port; + + if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP) { + audit_net.dport = port; + audit_net.v4info.daddr = addr4->sin_addr.s_addr; + } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP) { + audit_net.sport = port; + audit_net.v4info.saddr = addr4->sin_addr.s_addr; + } else { + WARN_ON_ONCE(1); + } break; + } #if IS_ENABLED(CONFIG_IPV6) - case AF_INET6: + case AF_INET6: { + const struct sockaddr_in6 *addr6; + if (addrlen < SIN6_LEN_RFC2133) return -EINVAL; - port = ((struct sockaddr_in6 *)address)->sin6_port; + + addr6 = (struct sockaddr_in6 *)address; + port = addr6->sin6_port; + + if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP) { + audit_net.dport = port; + audit_net.v6info.daddr = addr6->sin6_addr; + } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP) { + audit_net.sport = port; + audit_net.v6info.saddr = addr6->sin6_addr; + } else { + WARN_ON_ONCE(1); + } break; + } #endif /* IS_ENABLED(CONFIG_IPV6) */ default: @@ -149,6 +182,16 @@ static int current_check_access_socket(struct socket *const sock, ARRAY_SIZE(layer_masks))) return 0; + audit_net.family = address->sa_family; + landlock_log_denial(subject, + &(struct landlock_request){ + .type = LANDLOCK_REQUEST_NET_ACCESS, + .audit.type = LSM_AUDIT_DATA_NET, + .audit.u.net = &audit_net, + .access = access_request, + .layer_masks = &layer_masks, + .layer_masks_size = ARRAY_SIZE(layer_masks), + }); return -EACCES; } From patchwork Sat Mar 8 18:44:11 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007700 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-190d.mail.infomaniak.ch (smtp-190d.mail.infomaniak.ch [185.125.25.13]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C23FD198823 for ; Sat, 8 Mar 2025 18:50:11 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.125.25.13 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459814; cv=none; b=SYw4eYvoetVx7AuGirV2WYP6Wq1RStIAmxIiw2PB4hpbE6gatUaL5MkgpfsIoU9MPK0H2nfy3Th6KAewOsptlMl7Zql+7UJJbaHnnu6v/0YCbJ+TiYsZlJoI6BOMPC0xLQe3ONZsdk7MtXAbwCrPjAAbfluLGKuiZe9MjoyYYIU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459814; c=relaxed/simple; bh=cpV6J7vFv8QlFZY9S6XFDG63kp4XExRkTYYnDCOW2Zg=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=eOPjFKFipcJUboythx4H97MzazNSz0RFoh+FvtW1af9DQ+wAcSEo9CBpimhd6G4w7w72zXuA/uv1jxMoCOPIYikprQRJ+hiasV9Mhr03vX9j100P4TuapXQj0hYSgj7yhA51rOS7/TvAN3K1fsfmh5zwUmqnh+yhzIG1vwGmkxA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=Ts51/geC; arc=none smtp.client-ip=185.125.25.13 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="Ts51/geC" Received: from smtp-4-0000.mail.infomaniak.ch (smtp-4-0000.mail.infomaniak.ch [10.7.10.107]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsg6DYgzS6w; Sat, 8 Mar 2025 19:44:55 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459495; bh=v7zxGlR9p2Vk6aOhQ+9hHdnseZZyVA7sQx36u2Dx7Fs=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Ts51/geCAI90y4KlErY52fZVtW5rS8byJpifQ7ixm9wf9grvtX2gUw3HcgdeM4EsU UVXTvKtmLIecvxi2aJIec4JfZU9UfwtFy2xSz2YbKqKb9cFcaASrpCmgYOXF1yD1hP E7XTo20BVmZYCMDAVgIv2JwuEBWWsRc/OCfSAP7E= Received: from unknown by smtp-4-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsf6tT7zGbL; Sat, 8 Mar 2025 19:44:54 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 15/26] landlock: Log scoped denials Date: Sat, 8 Mar 2025 19:44:11 +0100 Message-ID: <20250308184422.2159360-16-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add audit support for unix_stream_connect, unix_may_send, task_kill, and file_send_sigiotask hooks. The related blockers are: - scope.abstract_unix_socket - scope.signal Audit event sample for abstract unix socket: type=LANDLOCK_DENY msg=audit(1729738800.268:30): domain=195ba459b blockers=scope.abstract_unix_socket path=00666F6F Audit event sample for signal: type=LANDLOCK_DENY msg=audit(1729738800.291:31): domain=195ba459b blockers=scope.signal opid=1 ocomm="systemd" Refactor and simplify error handling in LSM hooks. Extend struct landlock_file_security with fown_layer and use it to log the blocking domain. The struct aligned size is still 16 bytes. Cc: Günther Noack Cc: Tahera Fahimi Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-16-mic@digikod.net --- Changes since v5: - Move request declarations in the landlock_log_denial() calls to not impact allowed requests with audit, and return as soon as possible when access is allowed. - Store a fown_layer per file and use it to log the blocking domain. - Refactor and simplify error handling in LSM hooks. Changes since v4: - Rebase on top of the landlock_log_denial() and subject type changes. Changes since v3: - Cosmetic change to the "scope.*" blocker names. - Extend commit message. Changes since v1: - New patch. --- security/landlock/audit.c | 8 ++++ security/landlock/audit.h | 2 + security/landlock/fs.c | 7 +++- security/landlock/fs.h | 14 +++++++ security/landlock/task.c | 79 +++++++++++++++++++++++++++++++-------- 5 files changed, 92 insertions(+), 18 deletions(-) diff --git a/security/landlock/audit.c b/security/landlock/audit.c index 6c34758b9ff2..d0eafb946f31 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -71,6 +71,14 @@ get_blocker(const enum landlock_request_type type, if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(net_access_strings))) return "unknown"; return net_access_strings[access_bit]; + + case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET: + WARN_ON_ONCE(access_bit != -1); + return "scope.abstract_unix_socket"; + + case LANDLOCK_REQUEST_SCOPE_SIGNAL: + WARN_ON_ONCE(access_bit != -1); + return "scope.signal"; } WARN_ON_ONCE(1); diff --git a/security/landlock/audit.h b/security/landlock/audit.h index eeff2c5bfa4f..aaf21b31baa8 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -20,6 +20,8 @@ enum landlock_request_type { LANDLOCK_REQUEST_FS_CHANGE_LAYOUT, LANDLOCK_REQUEST_FS_ACCESS, LANDLOCK_REQUEST_NET_ACCESS, + LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET, + LANDLOCK_REQUEST_SCOPE_SIGNAL, }; /* diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 14f9c6d9903f..34f316031ecf 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1798,6 +1798,7 @@ static void hook_file_set_fowner(struct file *file) const struct landlock_cred_security *new_subject; struct landlock_cred_security *fown_subject; struct landlock_ruleset *prev_dom; + size_t handle_layer; /* * Lock already held by __f_setown(), see commit 26f204380a3c ("fs: Fix @@ -1807,15 +1808,17 @@ static void hook_file_set_fowner(struct file *file) fown_subject = &landlock_file(file)->fown_subject; prev_dom = fown_subject->domain; - new_subject = landlock_get_applicable_subject(current_cred(), - signal_scope, NULL); + new_subject = landlock_get_applicable_subject( + current_cred(), signal_scope, &handle_layer); if (new_subject) { landlock_get_ruleset(new_subject->domain); *fown_subject = *new_subject; + landlock_file(file)->fown_layer = handle_layer; } else { static const struct landlock_cred_security empty = {}; *fown_subject = empty; + landlock_file(file)->fown_layer = 0; } /* Called in an RCU read-side critical section. */ diff --git a/security/landlock/fs.h b/security/landlock/fs.h index 3a09ba985b74..a2a2014d68ae 100644 --- a/security/landlock/fs.h +++ b/security/landlock/fs.h @@ -9,6 +9,7 @@ #ifndef _SECURITY_LANDLOCK_FS_H #define _SECURITY_LANDLOCK_FS_H +#include #include #include #include @@ -61,6 +62,11 @@ struct landlock_file_security { * _LANDLOCK_ACCESS_FS_OPTIONAL). */ deny_masks_t deny_masks; + /** + * @fown_layer: Layer level of @fown_subject->domain with + * LANDLOCK_SCOPE_SIGNAL. + */ + u8 fown_layer; #endif /* CONFIG_AUDIT */ /** @@ -73,6 +79,14 @@ struct landlock_file_security { struct landlock_cred_security fown_subject; }; +#ifdef CONFIG_AUDIT + +/* Makes sure all layers can be identified. */ +static_assert((typeof_member(struct landlock_file_security, fown_layer))~0 >= + LANDLOCK_MAX_NUM_LAYERS); + +#endif /* CONFIG_AUDIT */ + /** * struct landlock_superblock_security - Superblock security blob * diff --git a/security/landlock/task.c b/security/landlock/task.c index e8a0b4ead381..f6359cf4c11b 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -264,26 +264,41 @@ static int hook_unix_stream_connect(struct sock *const sock, struct sock *const other, struct sock *const newsk) { + size_t handle_layer; const struct landlock_cred_security *const subject = landlock_get_applicable_subject(current_cred(), unix_scope, - NULL); + &handle_layer); /* Quick return for non-landlocked tasks. */ if (!subject) return 0; - if (is_abstract_socket(other) && sock_is_scoped(other, subject->domain)) - return -EPERM; + if (!is_abstract_socket(other)) + return 0; + + if (!sock_is_scoped(other, subject->domain)) + return 0; - return 0; + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET, + .audit = { + .type = LSM_AUDIT_DATA_NET, + .u.net = &(struct lsm_network_audit) { + .sk = other, + }, + }, + .layer_plus_one = handle_layer + 1, + }); + return -EPERM; } static int hook_unix_may_send(struct socket *const sock, struct socket *const other) { + size_t handle_layer; const struct landlock_cred_security *const subject = landlock_get_applicable_subject(current_cred(), unix_scope, - NULL); + &handle_layer); if (!subject) return 0; @@ -295,11 +310,23 @@ static int hook_unix_may_send(struct socket *const sock, if (unix_peer(sock->sk) == other->sk) return 0; - if (is_abstract_socket(other->sk) && - sock_is_scoped(other->sk, subject->domain)) - return -EPERM; + if (!is_abstract_socket(other->sk)) + return 0; + + if (!sock_is_scoped(other->sk, subject->domain)) + return 0; - return 0; + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET, + .audit = { + .type = LSM_AUDIT_DATA_NET, + .u.net = &(struct lsm_network_audit) { + .sk = other->sk, + }, + }, + .layer_plus_one = handle_layer + 1, + }); + return -EPERM; } static const struct access_masks signal_scope = { @@ -311,13 +338,15 @@ static int hook_task_kill(struct task_struct *const p, const struct cred *cred) { bool is_scoped; + size_t handle_layer; const struct landlock_cred_security *subject; if (!cred) /* Not dealing with USB IO. */ cred = current_cred(); - subject = landlock_get_applicable_subject(cred, signal_scope, NULL); + subject = landlock_get_applicable_subject(cred, signal_scope, + &handle_layer); /* Quick return for non-landlocked tasks. */ if (!subject) @@ -329,10 +358,19 @@ static int hook_task_kill(struct task_struct *const p, landlock_get_task_domain(p), signal_scope.scope); } - if (is_scoped) - return -EPERM; - return 0; + if (!is_scoped) + return 0; + + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_SCOPE_SIGNAL, + .audit = { + .type = LSM_AUDIT_DATA_TASK, + .u.tsk = p, + }, + .layer_plus_one = handle_layer + 1, + }); + return -EPERM; } static int hook_file_send_sigiotask(struct task_struct *tsk, @@ -361,10 +399,19 @@ static int hook_file_send_sigiotask(struct task_struct *tsk, landlock_get_task_domain(tsk), signal_scope.scope); } - if (is_scoped) - return -EPERM; - return 0; + if (!is_scoped) + return 0; + + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_SCOPE_SIGNAL, + .audit = { + .type = LSM_AUDIT_DATA_TASK, + .u.tsk = tsk, + }, + .layer_plus_one = landlock_file(fown->file)->fown_layer + 1, + }); + return -EPERM; } static struct security_hook_list landlock_hooks[] __ro_after_init = { From patchwork Sat Mar 8 18:44:12 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007680 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-bc0e.mail.infomaniak.ch (smtp-bc0e.mail.infomaniak.ch [45.157.188.14]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id DDD232153D4 for ; Sat, 8 Mar 2025 18:44:58 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=45.157.188.14 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459502; cv=none; b=fGtsRmSCSv0JvoVbBcLhL0Zi3EbREHEHcEbcPhEwA/92XD5gOQX20vOHW/qx+ab5oAY/R/kZp6859ZGd4sxZSprtAX+SlewhYlC4xXT+hM76PFtchBMEv8FafsxNbGATDY2VSKE8GUSQzPTK/tgMiKOaGraqXmcuoHuq/GCRAwQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459502; c=relaxed/simple; bh=k1m9iFSywrtIwR6hiRVlkDVU5un0WI0sVvyht2eqlcc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=ESXAtItan502phD00U9Z1G528AZDSNqgDygJGmslaw7iWA5DwrPjI+97lttJXGL5px7qBum7aWuves14EMC2JsmC2DfyMuSLjtAh4AHzLoNUgb+Dc6uhDoAAODmKoa/yadlp3TAtCtVYP2O/F63ywjpj+/ll3RJbKlBLIHxHWYk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=wT/LqKPN; arc=none smtp.client-ip=45.157.188.14 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="wT/LqKPN" Received: from smtp-3-0000.mail.infomaniak.ch (smtp-3-0000.mail.infomaniak.ch [10.4.36.107]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsj2hZrzQBn; Sat, 8 Mar 2025 19:44:57 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459497; bh=/wKf0zE+F9KVAAzytvTvsyiRmpBYc4ELYRrZOq7vhPQ=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=wT/LqKPNwF4iJTaHMAbHK06j/fUpmi5d3cqXcn0rHKGRbBd0xtW+VV6BEyHa/hzGz EWJ7Ny9X7YMTyY8HyKUrGr+LcIoMGzEC31aM6NQ4ZXQ0+GZXfCmDPXV7Jvgozz4NiK JmWFNLVEEYiAET7FApmwT9OZP6i+Lp+dSRDYvAc8= Received: from unknown by smtp-3-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsh3bCDzqYW; Sat, 8 Mar 2025 19:44:56 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 16/26] landlock: Add LANDLOCK_RESTRICT_SELF_LOG_*_EXEC_* flags Date: Sat, 8 Mar 2025 19:44:12 +0100 Message-ID: <20250308184422.2159360-17-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha TODO: Add header copyrights Most of the time we want to log denied access because they should not happen and such information helps diagnose issues. However, when sandboxing processes that we know will try to access denied resources (e.g. unknown, bogus, or malicious binary), we might want to not log related access requests that might fill up logs. By default, denied requests are logged until the task call execve(2). If the LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF flag is set, denied requests will not be logged for the same executed file. If the LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON flag is set, denied requests from after an execve(2) call will be logged. The rationale is that a program should know its own behavior, but not necessarily the behavior of other programs. Because LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF is set for a specific Landlock domain, it makes it possible to selectively mask some access requests that would be logged by a parent domain, which might be handy for unprivileged processes to limit logs. However, system administrators should still use the audit filtering mechanism. There is intentionally no audit nor sysctl configuration to re-enable these logs. This is delegated to the user space program. Increment the Landlock ABI version to reflect this interface change. Cc: Günther Noack Cc: Paul Moore Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-17-mic@digikod.net --- Changes since v5: - Rename LANDLOCK_RESTRICT_SELF_QUIET to LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, and adjust landlock_log_denial() accordingly. This better reflects what is the impact of this flag, especially wrt the following LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, and makes both flags compatible and complementary. - Rename LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC to LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON to be more consistent with other flags. - Change flag values. - Squash the LOG_SAME_EXEC_OFF and the LOG_NEW_EXEC_ON patches to get a more consistent patch. Changes since v4: - Rebase on top of the scoped guard patches. Changes since v3: - Rename LANDLOCK_RESTRICT_SELF_LOGLESS to LANDLOCK_RESTRICT_SELF_QUIET. "quiet" is already used by kernel's cmdline to disable most log messages, so this name makes sense for Landlock. - Improve the LANDLOCK_ABI_VERSION comment. Changes since v2: - Update ABI version test. --- include/uapi/linux/landlock.h | 21 ++++++++++++ security/landlock/audit.c | 16 +++++++-- security/landlock/domain.c | 2 ++ security/landlock/domain.h | 11 +++++++ security/landlock/limits.h | 4 +++ security/landlock/syscalls.c | 34 +++++++++++++++++--- tools/testing/selftests/landlock/base_test.c | 2 +- 7 files changed, 81 insertions(+), 9 deletions(-) diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h index e1d2c27533b4..daa1bc4123c3 100644 --- a/include/uapi/linux/landlock.h +++ b/include/uapi/linux/landlock.h @@ -4,6 +4,7 @@ * * Copyright © 2017-2020 Mickaël Salaün * Copyright © 2018-2020 ANSSI + * Copyright © 2021-2025 Microsoft Corporation */ #ifndef _UAPI_LINUX_LANDLOCK_H @@ -62,6 +63,26 @@ struct landlock_ruleset_attr { #define LANDLOCK_CREATE_RULESET_VERSION (1U << 0) /* clang-format on */ +/* + * sys_landlock_restrict_self() flags: + * + * - %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF: Do not create any log related to the + * enforced restrictions. This should only be set by tools launching unknown + * or untrusted programs (e.g. a sandbox tool, container runtime, system + * service manager). Because programs sandboxing themselves should fix any + * denied access, they should not set this flag to be aware of potential + * issues reported by system's logs (i.e. audit). + * - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON: Explicitly ask to continue + * logging denied access requests even after an :manpage:`execve(2)` call. + * This flag should only be set if all the programs than can legitimately be + * executed will not try to request a denied access (which could spam audit + * logs). + */ +/* clang-format off */ +#define LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF (1U << 0) +#define LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON (1U << 1) +/* clang-format on */ + /** * enum landlock_rule_type - Landlock rule type * diff --git a/security/landlock/audit.c b/security/landlock/audit.c index d0eafb946f31..01dc62ba7999 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -422,6 +422,9 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, get_hierarchy(subject->domain, youngest_layer); } + if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED) + return; + /* * Consistently keeps track of the number of denied access requests * even if audit is currently disabled, or if audit rules currently @@ -433,9 +436,16 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, if (!audit_enabled) return; - /* Ignores denials after an execution. */ - if (!(subject->domain_exec & (1 << youngest_layer))) - return; + /* Checks if the current exec was restricting itself. */ + if (subject->domain_exec & (1 << youngest_layer)) { + /* Ignores denials for the same execution. */ + if (!youngest_denied->log_same_exec) + return; + } else { + /* Ignores denials after a new execution. */ + if (!youngest_denied->log_cross_exec) + return; + } ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN, AUDIT_LANDLOCK_ACCESS); diff --git a/security/landlock/domain.c b/security/landlock/domain.c index 6704e9283206..925b3c2f8d23 100644 --- a/security/landlock/domain.c +++ b/security/landlock/domain.c @@ -127,6 +127,8 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) hierarchy->details = details; hierarchy->id = landlock_get_id_range(1); hierarchy->log_status = LANDLOCK_LOG_PENDING; + hierarchy->log_same_exec = true; + hierarchy->log_cross_exec = false; atomic64_set(&hierarchy->num_denials, 0); return 0; } diff --git a/security/landlock/domain.h b/security/landlock/domain.h index c1ab2fe1d441..567b86d48849 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -24,6 +24,7 @@ enum landlock_log_status { LANDLOCK_LOG_PENDING = 0, LANDLOCK_LOG_RECORDED, + LANDLOCK_LOG_DISABLED, }; /** @@ -103,6 +104,16 @@ struct landlock_hierarchy { * @details: Information about the related domain. */ const struct landlock_details *details; + /** + * @log_same_exec: Set if the domain is *not* configured with + * %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF. Set to true by default. + */ + u32 log_same_exec : 1, + /** + * @log_cross_exec: Set if the domain is configured with + * %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON. Set to false by default. + */ + log_cross_exec : 1; #endif /* CONFIG_AUDIT */ }; diff --git a/security/landlock/limits.h b/security/landlock/limits.h index 15f7606066c8..a45cd58898d0 100644 --- a/security/landlock/limits.h +++ b/security/landlock/limits.h @@ -29,6 +29,10 @@ #define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_SIGNAL #define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1) #define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE) + +#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON +#define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1) + /* clang-format on */ #endif /* _SECURITY_LANDLOCK_LIMITS_H */ diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index 5129981fec8b..c9f4e213a6f4 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -28,6 +28,7 @@ #include #include "cred.h" +#include "domain.h" #include "fs.h" #include "limits.h" #include "net.h" @@ -151,7 +152,14 @@ static const struct file_operations ruleset_fops = { .write = fop_dummy_write, }; -#define LANDLOCK_ABI_VERSION 6 +/* + * The Landlock ABI version should be incremented for each new Landlock-related + * user space visible change (e.g. Landlock syscalls). This version should + * only be incremented once per Linux release, and the date in + * Documentation/userspace-api/landlock.rst should be updated to reflect the + * UAPI change. + */ +#define LANDLOCK_ABI_VERSION 7 /** * sys_landlock_create_ruleset - Create a new ruleset @@ -429,7 +437,10 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd, * sys_landlock_restrict_self - Enforce a ruleset on the calling thread * * @ruleset_fd: File descriptor tied to the ruleset to merge with the target. - * @flags: Must be 0. + * @flags: Supported values: + * + * - %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF + * - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON * * This system call enables to enforce a Landlock ruleset on the current * thread. Enforcing a ruleset requires that the task has %CAP_SYS_ADMIN in its @@ -439,7 +450,7 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd, * Possible returned errors are: * * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; - * - %EINVAL: @flags is not 0. + * - %EINVAL: @flags contains an unknown bit. * - %EBADF: @ruleset_fd is not a file descriptor for the current thread; * - %EBADFD: @ruleset_fd is not a ruleset file descriptor; * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or the @@ -455,6 +466,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, *ruleset __free(landlock_put_ruleset) = NULL; struct cred *new_cred; struct landlock_cred_security *new_llcred; + bool log_same_exec, log_cross_exec; if (!is_initialized()) return -EOPNOTSUPP; @@ -467,10 +479,15 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) return -EPERM; - /* No flag for now. */ - if (flags) + if ((flags | LANDLOCK_MASK_RESTRICT_SELF) != + LANDLOCK_MASK_RESTRICT_SELF) return -EINVAL; + /* Translates "off" flag to boolean. */ + log_same_exec = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF); + /* Translates "on" flag to boolean. */ + log_cross_exec = !!(flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON); + /* Gets and checks the ruleset. */ ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ); if (IS_ERR(ruleset)) @@ -493,6 +510,13 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, return PTR_ERR(new_dom); } +#ifdef CONFIG_AUDIT + new_dom->hierarchy->log_same_exec = log_same_exec; + new_dom->hierarchy->log_cross_exec = log_cross_exec; + if (!log_same_exec && !log_cross_exec) + new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED; +#endif /* CONFIG_AUDIT */ + /* Replaces the old (prepared) domain. */ landlock_put_ruleset(new_llcred->domain); new_llcred->domain = new_dom; diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c index 1bc16fde2e8a..fbd687691b3c 100644 --- a/tools/testing/selftests/landlock/base_test.c +++ b/tools/testing/selftests/landlock/base_test.c @@ -76,7 +76,7 @@ TEST(abi_version) const struct landlock_ruleset_attr ruleset_attr = { .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, }; - ASSERT_EQ(6, landlock_create_ruleset(NULL, 0, + ASSERT_EQ(7, landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION)); ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0, From patchwork Sat Mar 8 18:44:13 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007681 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-190d.mail.infomaniak.ch (smtp-190d.mail.infomaniak.ch [185.125.25.13]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 875781F9F5C for ; Sat, 8 Mar 2025 18:45:00 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.125.25.13 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459503; cv=none; b=qjmF6qWHOMFZ5HA3J7L9dziHft9G6plXlhDrrC8LtfUKDVDt1UZKchpnuoIL91hhBEWJ0gfXz9BAYD5vmj4dqRzrDf9S9MkJeQ5gHXTxBCTf4nhxe7oFAnj6IXj038vz9CP803tEongsVhAojgKJ6fnxEXp851auD4wNWGtMYFA= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459503; c=relaxed/simple; bh=HY7qIKjBEs880v4uRMbaQluwbi4wIxUTYH16MjpJy7o=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=fT6BSIn4VS30YSJdhNEsXhtULQw0FeRNHC028U4ODIh9aJmASyjWcNSJhMx7A+q26lse70MZqRqrPY+9/seymaHMOUPsSseRcv6P1x84TNIgLd+LKOmN5p8IweLXf036ordtvsiFJqmjk7nX/PNHMEw1HWwa+ddRzcm6VX/zbrg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=nFFZP+XT; arc=none smtp.client-ip=185.125.25.13 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="nFFZP+XT" Received: from smtp-4-0001.mail.infomaniak.ch (unknown [IPv6:2001:1600:7:10:40ca:feff:fe05:1]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsk6QmqzQXN; Sat, 8 Mar 2025 19:44:58 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459498; bh=3viWkOI4qofVLlRfSbI1uxTIdc+LKxgzLDCR2tOktw0=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=nFFZP+XTehK97wbq/uScO1/+nU78acHZl61bgIRWx1dnVw32hQ4FFQeWqrN7nvZ4k QB/CQnkoPRL0segtfQ/vTpvD3erNzTDdP+GX3Z58OswEu76bAW1bOSJVw1MxdDslPW oL+RKssDyLU2PrKzw5enqai/efLNpMPD2/n6OAog= Received: from unknown by smtp-4-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsk11LQzhLM; Sat, 8 Mar 2025 19:44:58 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 17/26] landlock: Add LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF Date: Sat, 8 Mar 2025 19:44:13 +0100 Message-ID: <20250308184422.2159360-18-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF for the case of sandboxer tools, init systems, or runtime containers launching programs sandboxing themselves in an inconsistent way. Setting this flag should only depends on runtime configuration (i.e. not hardcoded). We don't create a new ruleset's option because this should not be part of the security policy: only the task that enforces the policy (not the one that create it) knows if itself or its children may request denied actions. This is the first and only flag that can be set without actually restricting the caller (i.e. without providing a ruleset). Extend struct landlock_cred_security with a u8 log_subdomains_off. struct landlock_file_security is still 16 bytes. Cc: Günther Noack Cc: Paul Moore Closes: https://github.com/landlock-lsm/linux/issues/3 Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-18-mic@digikod.net --- Changes since v5: - Rename LANDLOCK_RESTRICT_SELF_QUIET_SUBDOMAINS to LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF. As for LANDLOCK_RESTRICT_SELF_LOG_OFF, this makes more sense. - Store the log_subdomains bit in landlock_cred instead of landlock_hirerarchy because it is not directly related to the current domain. - Make it possible to set this flag without actually restricting the calling task, and update the related documentation. Changes since v4: - New patch. --- include/uapi/linux/landlock.h | 12 ++++++++++ security/landlock/cred.h | 7 ++++++ security/landlock/limits.h | 2 +- security/landlock/syscalls.c | 41 ++++++++++++++++++++++++++++++----- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h index daa1bc4123c3..66f6328583e4 100644 --- a/include/uapi/linux/landlock.h +++ b/include/uapi/linux/landlock.h @@ -77,10 +77,22 @@ struct landlock_ruleset_attr { * This flag should only be set if all the programs than can legitimately be * executed will not try to request a denied access (which could spam audit * logs). + * - %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF: Do not create any log related + * to the enforced restrictions coming from future nested domains created by + * the caller or its descendants. This should only be set according to a + * runtime configuration (i.e. not hardcoded) by programs launching other + * unknown or untrusted programs that may create their own Landlock domains + * and spam logs. The main use case is for container runtimes to enable users + * to mute buggy sandboxed programs for a specific container image. Other use + * cases include sandboxer tools and init systems. Unlike + * %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, + * %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF does not impact the requested + * restriction (if any) but only the future nested domains. */ /* clang-format off */ #define LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF (1U << 0) #define LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON (1U << 1) +#define LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF (1U << 2) /* clang-format on */ /** diff --git a/security/landlock/cred.h b/security/landlock/cred.h index cf38caf77adc..3194a46022f9 100644 --- a/security/landlock/cred.h +++ b/security/landlock/cred.h @@ -39,6 +39,13 @@ struct landlock_cred_security { * landlock_restrict_self(2)). */ u16 domain_exec; + /** + * @log_subdomains_off: Set if the domain descendants's log_status should be + * set to %LANDLOCK_LOG_DISABLED. This is not a landlock_hierarchy + * configuration because it applies to future descendant domains and it does + * not require a current domain. + */ + u8 log_subdomains_off : 1; #endif /* CONFIG_AUDIT */ } __packed; diff --git a/security/landlock/limits.h b/security/landlock/limits.h index a45cd58898d0..5c0dbf1beb48 100644 --- a/security/landlock/limits.h +++ b/security/landlock/limits.h @@ -30,7 +30,7 @@ #define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1) #define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE) -#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON +#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF #define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1) /* clang-format on */ diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index c9f4e213a6f4..e38b22075ca1 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -441,12 +441,16 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd, * * - %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF * - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON + * - %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF * * This system call enables to enforce a Landlock ruleset on the current * thread. Enforcing a ruleset requires that the task has %CAP_SYS_ADMIN in its * namespace or is running with no_new_privs. This avoids scenarios where * unprivileged tasks can affect the behavior of privileged children. * + * It is allowed to only pass the %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF + * flag with a @ruleset_fd value of -1. + * * Possible returned errors are: * * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; @@ -466,7 +470,8 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, *ruleset __free(landlock_put_ruleset) = NULL; struct cred *new_cred; struct landlock_cred_security *new_llcred; - bool log_same_exec, log_cross_exec; + bool log_same_exec, log_cross_exec, log_subdomains, + __maybe_unused prev_log_subdomains; if (!is_initialized()) return -EOPNOTSUPP; @@ -487,11 +492,20 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, log_same_exec = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF); /* Translates "on" flag to boolean. */ log_cross_exec = !!(flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON); + /* Translates "off" flag to boolean. */ + log_subdomains = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF); - /* Gets and checks the ruleset. */ - ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ); - if (IS_ERR(ruleset)) - return PTR_ERR(ruleset); + /* + * It is allowed to set %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with -1 + * as @ruleset_fd, but no other flag must be set. + */ + if (!(ruleset_fd == -1 && + flags == LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) { + /* Gets and checks the ruleset. */ + ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ); + if (IS_ERR(ruleset)) + return PTR_ERR(ruleset); + } /* Prepares new credentials. */ new_cred = prepare_creds(); @@ -500,6 +514,21 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, new_llcred = landlock_cred(new_cred); +#ifdef CONFIG_AUDIT + prev_log_subdomains = !new_llcred->log_subdomains_off; + new_llcred->log_subdomains_off = !prev_log_subdomains || + !log_subdomains; +#endif /* CONFIG_AUDIT */ + + /* + * The only case when a ruleset may not be set is if + * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set and ruleset_fd is -1. + * We could optimize this case by not calling commit_creds() if this flag + * was already set, but it is not worth the complexity. + */ + if (!ruleset) + return commit_creds(new_cred); + /* * There is no possible race condition while copying and manipulating * the current credentials because they are dedicated per thread. @@ -513,7 +542,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, #ifdef CONFIG_AUDIT new_dom->hierarchy->log_same_exec = log_same_exec; new_dom->hierarchy->log_cross_exec = log_cross_exec; - if (!log_same_exec && !log_cross_exec) + if ((!log_same_exec && !log_cross_exec) || !prev_log_subdomains) new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED; #endif /* CONFIG_AUDIT */ From patchwork Sat Mar 8 18:44:14 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007683 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-bc0b.mail.infomaniak.ch (smtp-bc0b.mail.infomaniak.ch [45.157.188.11]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 453E4215F79 for ; Sat, 8 Mar 2025 18:45:01 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=45.157.188.11 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459505; cv=none; b=q0rOOQ181+LDcPDkWJ1SQgmoTVbNP9Jr1eZuwrDhfiKmSz+EiTuJwkHDGoS82La4BsZdO9gewdDniE/nBwP3qi9/4ov1rfNKsr5BhyIoW2gKOJJzP4uE+5I4ewXzemjIlLIeO/c+vVhvuLdv5gQbU/9PecfCd61erL6uDmuHqs4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459505; c=relaxed/simple; bh=OlNzPmf82Dwqvaf790xzC1Jbc4nAdjQWZ7qF2fGhVDg=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=eFp+2KBo1qPu7md4kKmrXashQ92ha/UHXSkDfjk1v/pDD6L0AUF5uWmXY/HJGyIZV8Gh8eUXB8jgbeQyxrAAMTAf8sTIJiObZ6RWXoO8QHGyU3GBc2HfkdPtcV7MCTs8NUPOY/nGjgkrgWo4bM/ijzfkkOmVe9roFPK66XC5VHI= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=lHvSALqj; arc=none smtp.client-ip=45.157.188.11 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="lHvSALqj" Received: from smtp-4-0001.mail.infomaniak.ch (unknown [IPv6:2001:1600:7:10:40ca:feff:fe05:1]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsm2X6szSwT; Sat, 8 Mar 2025 19:45:00 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459500; bh=W9mq3JNdoLiJcn0ZPxWDmdxmjmG0hkY/Wsw30p5Lvpg=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=lHvSALqjk/H/JTp3r9ONV4PHn0PeYQ5rbPGoZec0/LM5X+V8n5Od3mXSaBMcW+Rd6 UVv6dTAdYprkf+ysmv6NINzZ+GPaJj1KnhH8z7o9kjh1/ATvL0h8BI2wk7ahXicukS VbwTHjYVT5YVRhFBAzAJwTjeC80/cnq/sgJ8Ps5k= Received: from unknown by smtp-4-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsl4HLbzjlG; Sat, 8 Mar 2025 19:44:59 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 18/26] samples/landlock: Enable users to log sandbox denials Date: Sat, 8 Mar 2025 19:44:14 +0100 Message-ID: <20250308184422.2159360-19-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha By default, denials from within the sandbox are not logged. Indeed, the sandboxer's security policy might not be fitted to the set of sandboxed processes that could be spawned (e.g. from a shell). For test purpose, parse the LL_FORCE_LOG environment variable to log every sandbox denials, including after launching the initial sandboxed program thanks to LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON. Cc: Günther Noack Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-19-mic@digikod.net --- Changes since v5: - Update with new flag. Changes since v3: - Extend error message, suggested by Francis Laniel. Changes since v2: - New patch. --- samples/landlock/sandboxer.c | 37 +++++++++++++++++++++++++++++++++--- security/landlock/syscalls.c | 8 ++------ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c index 07fab2ef534e..4e2854c6f9a3 100644 --- a/samples/landlock/sandboxer.c +++ b/samples/landlock/sandboxer.c @@ -58,6 +58,7 @@ static inline int landlock_restrict_self(const int ruleset_fd, #define ENV_TCP_BIND_NAME "LL_TCP_BIND" #define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT" #define ENV_SCOPED_NAME "LL_SCOPED" +#define ENV_FORCE_LOG_NAME "LL_FORCE_LOG" #define ENV_DELIMITER ":" static int str2num(const char *numstr, __u64 *num_dst) @@ -295,7 +296,7 @@ static bool check_ruleset_scope(const char *const env_var, /* clang-format on */ -#define LANDLOCK_ABI_LAST 6 +#define LANDLOCK_ABI_LAST 7 #define XSTR(s) #s #define STR(s) XSTR(s) @@ -322,6 +323,9 @@ static const char help[] = " - \"a\" to restrict opening abstract unix sockets\n" " - \"s\" to restrict sending signals\n" "\n" + "A sandboxer should not log denied access requests to avoid spamming logs, " + "but to test audit we can set " ENV_FORCE_LOG_NAME "=1\n" + "\n" "Example:\n" ENV_FS_RO_NAME "=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" " ENV_FS_RW_NAME "=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" " @@ -340,7 +344,7 @@ int main(const int argc, char *const argv[], char *const *const envp) const char *cmd_path; char *const *cmd_argv; int ruleset_fd, abi; - char *env_port_name; + char *env_port_name, *env_force_log; __u64 access_fs_ro = ACCESS_FS_ROUGHLY_READ, access_fs_rw = ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE; @@ -351,6 +355,8 @@ int main(const int argc, char *const argv[], char *const *const envp) .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL, }; + int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON; + int set_restrict_flags = 0; if (argc < 2) { fprintf(stderr, help, argv[0]); @@ -422,6 +428,13 @@ int main(const int argc, char *const argv[], char *const *const envp) /* Removes LANDLOCK_SCOPE_* for ABI < 6 */ ruleset_attr.scoped &= ~(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL); + __attribute__((fallthrough)); + case 6: + /* Removes LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON for ABI < 7 */ + supported_restrict_flags &= + ~LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON; + + /* Must be printed for any ABI < LANDLOCK_ABI_LAST. */ fprintf(stderr, "Hint: You should update the running kernel " "to leverage Landlock features " @@ -456,6 +469,24 @@ int main(const int argc, char *const argv[], char *const *const envp) if (check_ruleset_scope(ENV_SCOPED_NAME, &ruleset_attr)) return 1; + /* Enables optional logs. */ + env_force_log = getenv(ENV_FORCE_LOG_NAME); + if (env_force_log) { + if (strcmp(env_force_log, "1") != 0) { + fprintf(stderr, "Unknown value for " ENV_FORCE_LOG_NAME + " (only \"1\" is handled)\n"); + return 1; + } + if (!(supported_restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON)) { + fprintf(stderr, + "Audit logs not supported by current kernel\n"); + return 1; + } + set_restrict_flags |= LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON; + unsetenv(ENV_FORCE_LOG_NAME); + } + ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); if (ruleset_fd < 0) { @@ -483,7 +514,7 @@ int main(const int argc, char *const argv[], char *const *const envp) perror("Failed to restrict privileges"); goto err_close_ruleset; } - if (landlock_restrict_self(ruleset_fd, 0)) { + if (landlock_restrict_self(ruleset_fd, set_restrict_flags)) { perror("Failed to enforce ruleset"); goto err_close_ruleset; } diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index e38b22075ca1..cae0a0fd08d8 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -495,12 +495,8 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, /* Translates "off" flag to boolean. */ log_subdomains = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF); - /* - * It is allowed to set %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with -1 - * as @ruleset_fd, but no other flag must be set. - */ - if (!(ruleset_fd == -1 && - flags == LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) { + if (!(flags == LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF && + ruleset_fd == -1)) { /* Gets and checks the ruleset. */ ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ); if (IS_ERR(ruleset)) From patchwork Sat Mar 8 18:44:15 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007684 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-42ab.mail.infomaniak.ch (smtp-42ab.mail.infomaniak.ch [84.16.66.171]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 7B6652163AF; Sat, 8 Mar 2025 18:45:03 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=84.16.66.171 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459505; cv=none; b=D7Fk1A/Yx1ioZ+8anX8RR7+jYSQ7ymBEDHpASBgAeIUe2kZjnZRZ7xF1PrUBpEC2jnOe09RNmjuM2sa24dME0FFD1+zg37eYq3ZnJz0+bZBL55ZxbycGccn6LhGmi4PmlLj642T/bHu8HHBP6suBcoQRT0rEBUqsGSZDpV7TKVQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459505; c=relaxed/simple; bh=+mEtiRH9mg2PBl1dWGMUlkUQPx/inb4Y4ygUZfCMhnM=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=i97jE84Y6BUC1XVY2CoDzD9vLazUw/2c+nnbrv4jFK9jzdS9qT/+1jzPLj9aKD29YzI8RAe0nmEAF5qG9o62Q1hK9PdW/vaqG3owheAfsKpnX/h0Mke8odCYDWXDwW8rJyzwHnKd3PXyPQ5lYfzM8NZRUarfy0HJAW0SBSTwqWc= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=FKW0l1ej; arc=none smtp.client-ip=84.16.66.171 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="FKW0l1ej" Received: from smtp-4-0000.mail.infomaniak.ch (unknown [IPv6:2001:1600:7:10:40ca:feff:fe05:0]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsn6jSbzRWl; Sat, 8 Mar 2025 19:45:01 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459501; bh=eX6a/Otpuw4To8WlM9lxQqOqmo1WzQUlapzGb7manJs=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=FKW0l1ejWpVNelD5NPCKfyjChf9JgbkWqsOr/Yb4Lbl77RO5xN4UUJzsBgSa9hTCT q63HDz8llx0MgIZLsTkylPO0yjymGEP0DdStov+Q1Eabwmw6Lff5FjFN6bSwwyd2jG p9XtrjTr7OxR54dXwxH8T2aRSMxGViNsCok7g2gU= Received: from unknown by smtp-4-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsn0lNwzHQJ; Sat, 8 Mar 2025 19:45:01 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 19/26] selftests/landlock: Add test for invalid ruleset file descriptor Date: Sat, 8 Mar 2025 19:44:15 +0100 Message-ID: <20250308184422.2159360-20-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha To align with fs_test's layout1.inval and layout0.proc_nsfs which test EBADFD for landlock_add_rule(2), create a new base_test's restrict_self_fd which test EBADFD for landlock_restrict_self(2). Cc: Günther Noack Cc: Paul Moore Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-20-mic@digikod.net --- Changes since v5: - New standalone patch (that can be backported). --- tools/testing/selftests/landlock/base_test.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c index fbd687691b3c..9059045ae8b6 100644 --- a/tools/testing/selftests/landlock/base_test.c +++ b/tools/testing/selftests/landlock/base_test.c @@ -233,6 +233,17 @@ TEST(restrict_self_checks_ordering) ASSERT_EQ(0, close(ruleset_fd)); } +TEST(restrict_self_fd) +{ + int fd; + + fd = open("/dev/null", O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, fd); + + EXPECT_EQ(-1, landlock_restrict_self(fd, 0)); + EXPECT_EQ(EBADFD, errno); +} + TEST(ruleset_fd_io) { struct landlock_ruleset_attr ruleset_attr = { From patchwork Sat Mar 8 18:44:16 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007685 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-42ad.mail.infomaniak.ch (smtp-42ad.mail.infomaniak.ch [84.16.66.173]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E75F8202F9A for ; Sat, 8 Mar 2025 18:45:04 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=84.16.66.173 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459507; cv=none; b=MIUBSGoqyLmYIRhKeWjzZG/7F/NZAEZPFJCic73E9sPYC3m4YmsDLktvdHj0Bj8HMFxoGuYPqgVic4bAKnWq7vuH1eSMGqWykyABQ6QrQOJMNCggZye1QkWSJQkvajZyZETpv8K+zJAhvCYjsGqAweB5l1AHnlpKAiUZ21xAmn4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459507; c=relaxed/simple; bh=d1ahUO9jDK5pg7d/9PAjmipszcgEPlus3+ZqkBxJNkc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=mZB2/VKPXD+IXwp8tSEz+GtTdJxMuR3Tp/FpOEEij6ZERXp4+PsoqglMcAA3Pl+B8DbM0vMtc3b3ulAgXCBZ45C/74kIgTwvx0l+DcgapsO36yWo9FqlW1NjtxO0hGrIUqj1eNpWDcYX3ZpN+9YrcF6TnAldmAXkenL7Kv6pXkU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=Faw3SRV2; arc=none smtp.client-ip=84.16.66.173 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="Faw3SRV2" Received: from smtp-4-0000.mail.infomaniak.ch (smtp-4-0000.mail.infomaniak.ch [10.7.10.107]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsq2VkczLMc; Sat, 8 Mar 2025 19:45:03 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459503; bh=9AetjI08iDywqjQ4EG14lhpkZUde1COiJ/To5plRgo0=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Faw3SRV2wWKnSnl+Mhykh6nMk946Tk+q9WV+WHXmYxHyCJ0wmPzlIuQsDm+PJct0P W5li2iobh8e7l8VAQx+lG56/JS5e32K6RiIUzcdv5TGg9If4TlFT3QBo5XVp9u5qVI SNg79LDFyybYDIpL1BBH7D+2TFU7vQnNv2OEutEg= Received: from unknown by smtp-4-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsp3rTqzJyQ; Sat, 8 Mar 2025 19:45:02 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 20/26] selftests/landlock: Extend tests for landlock_restrict_self(2)'s flags Date: Sat, 8 Mar 2025 19:44:16 +0100 Message-ID: <20250308184422.2159360-21-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add the base_test's restrict_self_fd_flags tests to align with previous restrict_self_fd tests but with the new LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF flag. Add the restrict_self_flags tests to check that LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, and LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF are valid but not the next bit. Some checks are similar to restrict_self_checks_ordering's ones. Cc: Günther Noack Cc: Paul Moore Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-21-mic@digikod.net --- Changes since v5: - Rename restrict flags. - Update LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF test to pass with -1 as ruleset FD, and flag ordering. - Add restrict_self_fd_flags tests. Changes since v4: - Update with LANDLOCK_RESTRICT_SELF_QUIET_SUBDOMAINS, and LANDLOCK_RESTRICT_SELF_LOG_CROSS_EXEC. Changes since v3: - Use a last_flag variable. Changes since v2: - New patch. --- tools/testing/selftests/landlock/base_test.c | 71 ++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c index 9059045ae8b6..ab00afe1fc25 100644 --- a/tools/testing/selftests/landlock/base_test.c +++ b/tools/testing/selftests/landlock/base_test.c @@ -244,6 +244,77 @@ TEST(restrict_self_fd) EXPECT_EQ(EBADFD, errno); } +TEST(restrict_self_fd_flags) +{ + int fd; + + fd = open("/dev/null", O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, fd); + + /* + * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF accepts -1 but not any file + * descriptor. + */ + EXPECT_EQ(-1, landlock_restrict_self( + fd, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)); + EXPECT_EQ(EBADFD, errno); +} + +TEST(restrict_self_flags) +{ + const __u32 last_flag = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF; + + /* Tests invalid flag combinations. */ + + EXPECT_EQ(-1, landlock_restrict_self(-1, last_flag << 1)); + EXPECT_EQ(EINVAL, errno); + + EXPECT_EQ(-1, landlock_restrict_self(-1, -1)); + EXPECT_EQ(EINVAL, errno); + + /* Tests valid flag combinations. */ + + EXPECT_EQ(-1, landlock_restrict_self(-1, 0)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(-1, landlock_restrict_self( + -1, LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(-1, + landlock_restrict_self( + -1, + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF | + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(-1, + landlock_restrict_self( + -1, + LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON | + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(-1, landlock_restrict_self( + -1, LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(-1, + landlock_restrict_self( + -1, LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF | + LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON)); + EXPECT_EQ(EBADF, errno); + + /* Tests with an invalid ruleset_fd. */ + + EXPECT_EQ(-1, landlock_restrict_self( + -2, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(0, landlock_restrict_self( + -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)); +} + TEST(ruleset_fd_io) { struct landlock_ruleset_attr ruleset_attr = { From patchwork Sat Mar 8 18:44:17 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007686 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-bc0c.mail.infomaniak.ch (smtp-bc0c.mail.infomaniak.ch [45.157.188.12]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 18A3C1DC99E for ; Sat, 8 Mar 2025 18:45:06 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=45.157.188.12 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459509; cv=none; b=MOvwV4nphohNVtwZG8A4Pifr/tsw1/7qTRHEGNBMWnTpsiFXdRoRTDjEg4k1md3V3YXyD2blx4rW9jMNBqYK/9gfP+dAPNZCBwIgOWeJ2wjO9ExTs3k0Cb4T58Ke7of0JC+HoprbSrqVKHHlt+GRw/JkatAnRt+X3HwZgYjz9Hc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459509; c=relaxed/simple; bh=7cpt9k1ZLpaoZVPQS1W5YzD18ADWhCuTc32JA0fb3Js=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=PUWr+d+mVGsyCaOHgHztqAy4K568GRIVZf7Wrh+oJywtbqVJMX4cXsQgHBX6TC4BpvAkLbWLWfCQtEluJeN73buSMUgtDca8pHQxfGH9BKXE3rQrxskbsEPXdkrjAiubtYSeRtwckRNLUaW3bbkPkour9BTtkUK8fiUvcicKI+w= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=JwinksjP; arc=none smtp.client-ip=45.157.188.12 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="JwinksjP" Received: from smtp-3-0000.mail.infomaniak.ch (smtp-3-0000.mail.infomaniak.ch [10.4.36.107]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsr59R8zS5n; Sat, 8 Mar 2025 19:45:04 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459504; bh=Fldv1rn6sHfI9yybhspBhN5Y364HcxA84Nrn3knPH98=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=JwinksjPwsH+yBtvX0UpO6KmKi+0zQ/+BZ2yLZNrluySswCbyykLQ6tjx6bVYLcFx tMHXZpqtRczv0gyNMIAnuYCGEkc0uME3E8OGOnYWmrjdtTUtmuFSCzBKNlMUgCXKva 2yP3p0Bf0Rd1QNAWlBpimYtK/9GnrtXt1Nfbx/wE= Received: from unknown by smtp-3-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsq6pgVzqrM; Sat, 8 Mar 2025 19:45:03 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 21/26] selftests/landlock: Add tests for audit flags and domain IDs Date: Sat, 8 Mar 2025 19:44:17 +0100 Message-ID: <20250308184422.2159360-22-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add audit_test.c to check with and without LANDLOCK_RESTRICT_SELF_* flags against the two Landlock audit record types: AUDIT_LANDLOCK_ACCESS and AUDIT_LANDLOCK_DOMAIN. Check consistency of domain IDs in AUDIT_LANDLOCK_ACCESS and AUDIT_LANDLOCK_DOMAIN messages: denied access, domain allocation, and domain deallocation. Tests are run with audit filters to ensure the audit records come from the test program. Moreover, because there can only be one audit process, tests would failed if run in parallel. Because of audit limitations, tests can only be run in the initial namespace. The audit test helpers were inspired by libaudit and tools/testing/selftests/net/netfilter/audit_logread.c Cc: Günther Noack Cc: Paul Moore Cc: Phil Sutter Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-22-mic@digikod.net --- There is a race condition when checking the "domain drop" event on some systems. Changes since v5: - Enhance audit_match_record() with an __u64 *domain_id argument set according to the audit logs. - Rename audit_fork.flags to audit_flags.signal . - Rename variants according to new flags. - Check consistency of domain IDs. Changes since v4: - Update with the new landlock_restrict_self()'s flags, the new audit rule types, and message fields. - Simplify audit_filter_exe() and audit_init_filter_exe(). - Test with kill() instead of umount(). - Test domain deallocation events. Changes since v3: - Improve audit_request() to check Netlink errors and handle multiple replies. - Make audit_filter_exe() more generic to handle several audit rule lists. - Merge audit_init_state() into audit_init() and create audit_init_with_exe_filter() to handle AUDIT_EXE_LANDLOCK_DENY with an arbitrary path. - Add matches_log_dom_info(). Changes since v2: - New patch. --- tools/testing/selftests/landlock/audit.h | 386 ++++++++++++++++++ tools/testing/selftests/landlock/audit_test.c | 238 +++++++++++ tools/testing/selftests/landlock/common.h | 2 + tools/testing/selftests/landlock/config | 1 + 4 files changed, 627 insertions(+) create mode 100644 tools/testing/selftests/landlock/audit.h create mode 100644 tools/testing/selftests/landlock/audit_test.c diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h new file mode 100644 index 000000000000..b3b3e52e8256 --- /dev/null +++ b/tools/testing/selftests/landlock/audit.h @@ -0,0 +1,386 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Landlock audit helpers + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef ARRAY_SIZE +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) +#endif + +#define REGEX_LANDLOCK_PREFIX "^audit([0-9.:]\\+): domain=\\([0-9a-f]\\+\\)" + +struct audit_filter { + __u32 record_type; + size_t exe_len; + char exe[PATH_MAX]; +}; + +struct audit_message { + struct nlmsghdr header; + union { + struct audit_status status; + struct audit_features features; + struct audit_rule_data rule; + struct nlmsgerr err; + char data[PATH_MAX + 200]; + }; +}; + +static int audit_send(const int fd, const struct audit_message *const msg) +{ + struct sockaddr_nl addr = { + .nl_family = AF_NETLINK, + }; + int ret; + + do { + ret = sendto(fd, msg, msg->header.nlmsg_len, 0, + (struct sockaddr *)&addr, sizeof(addr)); + } while (ret < 0 && errno == EINTR); + + if (ret < 0) + return -errno; + + if (ret != msg->header.nlmsg_len) + return -E2BIG; + + return 0; +} + +static int audit_recv(const int fd, struct audit_message *msg) +{ + struct sockaddr_nl addr; + socklen_t addrlen = sizeof(addr); + struct audit_message msg_tmp; + int err; + + if (!msg) + msg = &msg_tmp; + + do { + err = recvfrom(fd, msg, sizeof(*msg), 0, + (struct sockaddr *)&addr, &addrlen); + } while (err < 0 && errno == EINTR); + + if (err < 0) + return -errno; + + if (addrlen != sizeof(addr) || addr.nl_pid != 0) + return -EINVAL; + + /* Checks Netlink error or end of messages. */ + if (msg->header.nlmsg_type == NLMSG_ERROR) + return msg->err.error; + + return 0; +} + +static int audit_request(const int fd, + const struct audit_message *const request, + struct audit_message *reply) +{ + struct audit_message msg_tmp; + bool first_reply = true; + int err; + + err = audit_send(fd, request); + if (err) + return err; + + if (!reply) + reply = &msg_tmp; + + do { + if (first_reply) + first_reply = false; + else + reply = &msg_tmp; + + err = audit_recv(fd, reply); + if (err) + return err; + } while (reply->header.nlmsg_type != NLMSG_ERROR && + reply->err.msg.nlmsg_type != request->header.nlmsg_type); + + return reply->err.error; +} + +static int audit_filter_exe(const int audit_fd, + const struct audit_filter *const filter, + const __u16 type) +{ + struct audit_message msg = { + .header = { + .nlmsg_len = NLMSG_SPACE(sizeof(msg.rule)) + + NLMSG_ALIGN(filter->exe_len), + .nlmsg_type = type, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, + }, + .rule = { + .flags = AUDIT_FILTER_EXCLUDE, + .action = AUDIT_NEVER, + .field_count = 1, + .fields[0] = filter->record_type, + .fieldflags[0] = AUDIT_NOT_EQUAL, + .values[0] = filter->exe_len, + .buflen = filter->exe_len, + } + }; + + if (filter->record_type != AUDIT_EXE) + return -EINVAL; + + memcpy(msg.rule.buf, filter->exe, filter->exe_len); + return audit_request(audit_fd, &msg, NULL); +} + +static int audit_filter_drop(const int audit_fd, const __u16 type) +{ + struct audit_message msg = { + .header = { + .nlmsg_len = NLMSG_SPACE(sizeof(msg.rule)), + .nlmsg_type = type, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, + }, + .rule = { + .flags = AUDIT_FILTER_EXCLUDE, + .action = AUDIT_NEVER, + .field_count = 1, + .fields[0] = AUDIT_MSGTYPE, + .fieldflags[0] = AUDIT_NOT_EQUAL, + .values[0] = AUDIT_LANDLOCK_DOMAIN, + } + }; + + return audit_request(audit_fd, &msg, NULL); +} + +static int audit_set_status(int fd, __u32 key, __u32 val) +{ + const struct audit_message msg = { + .header = { + .nlmsg_len = NLMSG_SPACE(sizeof(msg.status)), + .nlmsg_type = AUDIT_SET, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, + }, + .status = { + .mask = key, + .enabled = key == AUDIT_STATUS_ENABLED ? val : 0, + .pid = key == AUDIT_STATUS_PID ? val : 0, + } + }; + + return audit_request(fd, &msg, NULL); +} + +/* + * @domain_id: The domain ID extracted from the audit message (if the first part + * of @pattern is REGEX_LANDLOCK_PREFIX). It is set to 0 if the domain ID is + * not found. + */ +static int audit_match_record(int audit_fd, const __u16 type, + const char *const pattern, __u64 *domain_id) +{ + struct audit_message msg; + int ret, err = 0; + bool matches_record = !type; + regmatch_t matches[2]; + regex_t regex; + + ret = regcomp(®ex, pattern, 0); + if (ret) + return -EINVAL; + + do { + memset(&msg, 0, sizeof(msg)); + err = audit_recv(audit_fd, &msg); + if (err) + goto out; + + if (msg.header.nlmsg_type == type) + matches_record = true; + } while (!matches_record); + + ret = regexec(®ex, msg.data, ARRAY_SIZE(matches), matches, 0); + if (ret) { + printf("DATA: %s\n", msg.data); + printf("ERROR: no match for pattern: %s\n", pattern); + err = -ENOENT; + } + + if (domain_id) { + *domain_id = 0; + if (matches[1].rm_so != -1) { + int match_len = matches[1].rm_eo - matches[1].rm_so; + /* The maximal characters of a 2^64 hexadecimal number is 17. */ + char dom_id[18]; + + if (match_len > 0 && match_len < sizeof(dom_id)) { + memcpy(dom_id, msg.data + matches[1].rm_so, + match_len); + dom_id[match_len] = '\0'; + if (domain_id) + *domain_id = strtoull(dom_id, NULL, 16); + } + } + } + +out: + regfree(®ex); + return err; +} + +struct audit_records { + size_t access; + size_t domain; +}; + +// TODO: check that audit_fd is valid +static void audit_count_records(int audit_fd, struct audit_records *records) +{ + struct audit_message msg; + + records->access = 0; + records->domain = 0; + + do { + memset(&msg, 0, sizeof(msg)); + if (audit_recv(audit_fd, &msg)) + return; + + switch (msg.header.nlmsg_type) { + case AUDIT_LANDLOCK_ACCESS: + records->access++; + break; + case AUDIT_LANDLOCK_DOMAIN: + records->domain++; + break; + } + } while (true); +} + +static int audit_init(void) +{ + const struct timeval tv = { + .tv_usec = 1, + }; + int fd, err; + + fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_AUDIT); + if (fd < 0) + return -errno; + + err = audit_set_status(fd, AUDIT_STATUS_ENABLED, 1); + if (err) + return err; + + err = audit_set_status(fd, AUDIT_STATUS_PID, getpid()); + if (err) + return err; + + /* Sets a timeout for negative tests. */ + err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + if (err) + return -errno; + + return fd; +} + +static int audit_init_filter_exe(struct audit_filter *filter, const char *path) +{ + char *absolute_path = NULL; + + // TODO: Make sure there is no rule already filtering. + + filter->record_type = AUDIT_EXE; + + if (!path) { + filter->exe_len = readlink("/proc/self/exe", filter->exe, + sizeof(filter->exe) - 1); + if (filter->exe_len < 0) + return -errno; + + return 0; + } + + absolute_path = realpath(path, NULL); + if (!absolute_path) + return -errno; + + /* No need for the terminating NULL byte. */ + filter->exe_len = strlen(absolute_path); + if (filter->exe_len > sizeof(filter->exe)) + return -E2BIG; + + memcpy(filter->exe, absolute_path, filter->exe_len); + free(absolute_path); + return 0; +} + +static int audit_cleanup(int audit_fd, struct audit_filter *filter) +{ + struct audit_filter new_filter; + + if (audit_fd < 0 || !filter) { + int err; + + /* + * Simulates audit_init_with_exe_filter() when called from + * FIXTURE_TEARDOWN_PARENT(). + */ + audit_fd = audit_init(); + if (audit_fd < 0) + return audit_fd; + + filter = &new_filter; + err = audit_init_filter_exe(filter, NULL); + if (err) + return err; + } + + /* Filters might not be in place. */ + audit_filter_exe(audit_fd, filter, AUDIT_DEL_RULE); + audit_filter_drop(audit_fd, AUDIT_DEL_RULE); + + /* + * Because audit_cleanup() might not be called by the test auditd + * process, it might not be possible to explicitly set it. Anyway, + * AUDIT_STATUS_ENABLED will implicitly be set to 0 when the auditd + * process will exit. + */ + return close(audit_fd); +} + +static int audit_init_with_exe_filter(struct audit_filter *filter) +{ + int fd, err; + + fd = audit_init(); + if (fd < 0) + return fd; + + err = audit_init_filter_exe(filter, NULL); + if (err) + return err; + + err = audit_filter_exe(fd, filter, AUDIT_ADD_RULE); + if (err) + return err; + + return fd; +} diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c new file mode 100644 index 000000000000..09bf74749aeb --- /dev/null +++ b/tools/testing/selftests/landlock/audit_test.c @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Audit + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#include "audit.h" +#include "common.h" + +static int matches_log_domain_allocated(struct __test_metadata *const _metadata, + int audit_fd, __u64 *domain_id) +{ + return audit_match_record( + audit_fd, AUDIT_LANDLOCK_DOMAIN, + REGEX_LANDLOCK_PREFIX + " status=allocated mode=enforcing pid=[0-9]\\+ uid=[0-9]\\+" + " exe=\"[^\"]\\+\" comm=\"audit_test\"$", + domain_id); +} + +static int +matches_log_domain_deallocated(struct __test_metadata *const _metadata, + int audit_fd, unsigned int num_denials, + __u64 *domain_id) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " status=deallocated denials=%u$"; + char log_match[sizeof(log_template) + 10]; + int log_match_len; + + log_match_len = snprintf(log_match, sizeof(log_match), log_template, + num_denials); + if (log_match_len > sizeof(log_match)) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match, + domain_id); +} + +static int matches_log_signal(struct __test_metadata *const _metadata, + int audit_fd, const pid_t opid, __u64 *domain_id) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " blockers=scope\\.signal opid=%d ocomm=\"audit_test\"$"; + char log_match[sizeof(log_template) + 10]; + int log_match_len; + + log_match_len = + snprintf(log_match, sizeof(log_match), log_template, opid); + if (log_match_len > sizeof(log_match)) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match, + domain_id); +} + +FIXTURE(audit_flags) +{ + struct audit_filter audit_filter; + int audit_fd; + __u64 *domain_id; +}; + +FIXTURE_VARIANT(audit_flags) +{ + const int restrict_flags; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, default) { + /* clang-format on */ + .restrict_flags = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, same_exec_off) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, subdomains_off) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, cross_exec_on) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, +}; + +FIXTURE_SETUP(audit_flags) +{ + disable_caps(_metadata); + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + EXPECT_LE(0, self->audit_fd) + { + const char *error_msg; + + /* kill "$(auditctl -s | sed -ne 's/^pid \([0-9]\+\)$/\1/p')" */ + if (self->audit_fd == -EEXIST) + error_msg = "socket already in use (e.g. auditd)"; + else + error_msg = strerror(-self->audit_fd); + TH_LOG("Failed to initialize audit: %s", error_msg); + } + clear_cap(_metadata, CAP_AUDIT_CONTROL); + + self->domain_id = mmap(NULL, sizeof(*self->domain_id), + PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(MAP_FAILED, self->domain_id); + /* Domain IDs are greater or equal to 2^32. */ + *self->domain_id = 1; +} + +FIXTURE_TEARDOWN(audit_flags) +{ + EXPECT_EQ(0, munmap(self->domain_id, sizeof(*self->domain_id))); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_cleanup(self->audit_fd, &self->audit_filter)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +TEST_F(audit_flags, signal) +{ + int status; + pid_t child; + struct audit_records records; + __u64 domain_id_deallocated = 2; + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + int ruleset_fd; + + /* Add filesystem restrictions. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, + variant->restrict_flags)); + EXPECT_EQ(0, close(ruleset_fd)); + + /* First signal checks to test log entries. */ + EXPECT_EQ(-1, kill(getppid(), 0)); + EXPECT_EQ(EPERM, errno); + + if (variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) { + EXPECT_EQ(-EAGAIN, matches_log_signal( + _metadata, self->audit_fd, + getppid(), self->domain_id)); + EXPECT_EQ(*self->domain_id, 1); + } else { + __u64 domain_id_allocated; + + EXPECT_EQ(0, matches_log_signal( + _metadata, self->audit_fd, + getppid(), self->domain_id)); + + /* Checks domain information records. */ + EXPECT_EQ(0, matches_log_domain_allocated( + _metadata, self->audit_fd, + &domain_id_allocated)); + EXPECT_NE(*self->domain_id, 1); + EXPECT_NE(*self->domain_id, 0); + EXPECT_EQ(*self->domain_id, domain_id_allocated); + } + + /* Second signal checks to test audit_count_records(). */ + EXPECT_EQ(-1, kill(getppid(), 0)); + EXPECT_EQ(EPERM, errno); + + /* Makes sure there is no superfluous logged records. */ + audit_count_records(self->audit_fd, &records); + if (variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) { + EXPECT_EQ(0, records.access); + } else { + EXPECT_EQ(1, records.access); + } + EXPECT_EQ(0, records.domain); + + /* Updates filter rules to match the drop record. */ + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_filter_drop(self->audit_fd, AUDIT_ADD_RULE)); + EXPECT_EQ(0, + audit_filter_exe(self->audit_fd, &self->audit_filter, + AUDIT_DEL_RULE)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); + + _exit(_metadata->exit_code); + return; + } + + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; + + if (variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) { + EXPECT_EQ(-EAGAIN, matches_log_domain_deallocated( + _metadata, self->audit_fd, 0, + &domain_id_deallocated)); + EXPECT_EQ(domain_id_deallocated, 2); + } else { + // FIXME: Even if we waited for the child, the following call always + // return -EAGAIN on some environments unless we call sleep(1). + // Any idea how to avoid that? + EXPECT_EQ(0, matches_log_domain_deallocated( + _metadata, self->audit_fd, 2, + &domain_id_deallocated)); + EXPECT_NE(domain_id_deallocated, 2); + EXPECT_NE(domain_id_deallocated, 0); + EXPECT_EQ(domain_id_deallocated, *self->domain_id); + } +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h index 6064c9ac0532..fe6ada2c2518 100644 --- a/tools/testing/selftests/landlock/common.h +++ b/tools/testing/selftests/landlock/common.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ static void _init_caps(struct __test_metadata *const _metadata, bool drop_all) /* Only these three capabilities are useful for the tests. */ const cap_value_t caps[] = { /* clang-format off */ + CAP_AUDIT_CONTROL, CAP_DAC_OVERRIDE, CAP_MKNOD, CAP_NET_ADMIN, diff --git a/tools/testing/selftests/landlock/config b/tools/testing/selftests/landlock/config index 425de4c20271..8fe9b461b1fd 100644 --- a/tools/testing/selftests/landlock/config +++ b/tools/testing/selftests/landlock/config @@ -1,4 +1,5 @@ CONFIG_AF_UNIX_OOB=y +CONFIG_AUDIT=y CONFIG_CGROUPS=y CONFIG_CGROUP_SCHED=y CONFIG_INET=y From patchwork Sat Mar 8 18:44:18 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007687 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-8fa8.mail.infomaniak.ch (smtp-8fa8.mail.infomaniak.ch [83.166.143.168]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id B10632185AB for ; Sat, 8 Mar 2025 18:45:07 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=83.166.143.168 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459511; cv=none; b=g1ACFQNQNRcsp0zmP5KrwRql7KRZ2sNccxVmFyaE6/GQb+a5mMKTFfm8l+QokmV6jushoTwdiLPPjrMqBjl7nFzKvjKSGk99pFnimB5/JS2AN3kr89xYymVjNcw2yZdK+P96CYrjoKsL++qrl591yX2gG79mKJZr/lfpMObEU44= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459511; c=relaxed/simple; bh=bkKBqEr4/tKrYDJa27hS/Le2FQozkPU3j6DQFXZujkQ=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=sl1mNuLVBGFMvGbaAjdb7kUkFKy+PUcIPFQVmzPimKsY03IyZOtdvkHvaKmR0CTZsP2yo2JJzGodU0sSOlwchVwGrIOGWltGcYiLIENvbm6XHGtgmJCBoq/cWrI6gjpkG9H6tpenZRdxt8EMKizNYMsyg/42EZjd+QnEJyw9FdU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=EYI8f4C7; arc=none smtp.client-ip=83.166.143.168 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="EYI8f4C7" Received: from smtp-3-0001.mail.infomaniak.ch (smtp-3-0001.mail.infomaniak.ch [10.4.36.108]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bst1NTSzQ63; Sat, 8 Mar 2025 19:45:06 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459506; bh=TE5apQ1dXV6R5Q8WkyJscCJUT5zZddaiecZQit9SOwA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=EYI8f4C7YEyfnzdILH7J8y87CC1qhncfVuPRzagIfyhcvfv4xPSpFH4S3q5Q8/+Uq UQcaMd4FWB8z5cdoGtNIPViNKBLrqc2HqFLyfFnbyo5QpKrbr9pwxKjLVx6Mc4zUI1 gYLxq/IifdQ6M3OiKH7pJXb7wzvQYtaBFkY0d3LQ= Received: from unknown by smtp-3-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bss31SSz28w; Sat, 8 Mar 2025 19:45:05 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 22/26] selftests/landlock: Test audit with restrict flags Date: Sat, 8 Mar 2025 19:44:18 +0100 Message-ID: <20250308184422.2159360-23-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add audit_exec tests to filter Landlock denials according to cross-execution or muted subdomains. Add a wait-pipe-sandbox.c test program to sandbox itself and send a (denied) signals to its parent. Cc: Günther Noack Cc: Paul Moore Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-23-mic@digikod.net --- Changes since v5: - Rename audit_exec.flags to audit_exec.signal_and_open . Changes since v4: - Revamp to test the Landlock syscall flags instead of the audit rules. - Copy wait-pipe.c to wait-pipe-sandbox.c and extend it. - Fix regex. Changes since v3: - New patch. --- tools/testing/selftests/landlock/Makefile | 6 +- tools/testing/selftests/landlock/audit_test.c | 222 ++++++++++++++++++ tools/testing/selftests/landlock/common.h | 1 + .../selftests/landlock/wait-pipe-sandbox.c | 131 +++++++++++ 4 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 tools/testing/selftests/landlock/wait-pipe-sandbox.c diff --git a/tools/testing/selftests/landlock/Makefile b/tools/testing/selftests/landlock/Makefile index 5cb0828f0514..a3f449914bf9 100644 --- a/tools/testing/selftests/landlock/Makefile +++ b/tools/testing/selftests/landlock/Makefile @@ -10,7 +10,11 @@ src_test := $(wildcard *_test.c) TEST_GEN_PROGS := $(src_test:.c=) -TEST_GEN_PROGS_EXTENDED := true sandbox-and-launch wait-pipe +TEST_GEN_PROGS_EXTENDED := \ + true \ + sandbox-and-launch \ + wait-pipe \ + wait-pipe-sandbox # Short targets: $(TEST_GEN_PROGS): LDLIBS += -lcap -lpthread diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c index 09bf74749aeb..58ec0ee15af3 100644 --- a/tools/testing/selftests/landlock/audit_test.c +++ b/tools/testing/selftests/landlock/audit_test.c @@ -7,7 +7,9 @@ #define _GNU_SOURCE #include +#include #include +#include #include #include #include @@ -64,6 +66,16 @@ static int matches_log_signal(struct __test_metadata *const _metadata, domain_id); } +static int matches_log_fs_read_root(struct __test_metadata *const _metadata, + int audit_fd) +{ + return audit_match_record( + audit_fd, AUDIT_LANDLOCK_ACCESS, + REGEX_LANDLOCK_PREFIX + " blockers=fs\\.read_dir path=\"/\" dev=\"[^\"]\\+\" ino=[0-9]\\+$", + NULL); +} + FIXTURE(audit_flags) { struct audit_filter audit_filter; @@ -235,4 +247,214 @@ TEST_F(audit_flags, signal) } } +FIXTURE(audit_exec) +{ + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_VARIANT(audit_exec) +{ + const int restrict_flags; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_exec, default) { + /* clang-format on */ + .restrict_flags = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_exec, same_exec_off) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_exec, subdomains_off) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_exec, cross_exec_on) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_exec, subdomains_off_and_cross_exec_on) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF | + LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, +}; + +FIXTURE_SETUP(audit_exec) +{ + disable_caps(_metadata); + set_cap(_metadata, CAP_AUDIT_CONTROL); + + self->audit_fd = audit_init(); + EXPECT_LE(0, self->audit_fd) + { + const char *error_msg; + + /* kill "$(auditctl -s | sed -ne 's/^pid \([0-9]\+\)$/\1/p')" */ + if (self->audit_fd == -EEXIST) + error_msg = "socket already in use (e.g. auditd)"; + else + error_msg = strerror(-self->audit_fd); + TH_LOG("Failed to initialize audit: %s", error_msg); + } + + /* Applies test filter for the bin_wait_pipe_sandbox program. */ + EXPECT_EQ(0, audit_init_filter_exe(&self->audit_filter, + bin_wait_pipe_sandbox)); + EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter, + AUDIT_ADD_RULE)); + + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +FIXTURE_TEARDOWN(audit_exec) +{ + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter, + AUDIT_DEL_RULE)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, close(self->audit_fd)); +} + +TEST_F(audit_exec, signal_and_open) +{ + struct audit_records records; + int pipe_child[2], pipe_parent[2]; + char buf_parent; + pid_t child; + int status; + + ASSERT_EQ(0, pipe2(pipe_child, 0)); + ASSERT_EQ(0, pipe2(pipe_parent, 0)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + const struct landlock_ruleset_attr layer1 = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + char pipe_child_str[12], pipe_parent_str[12]; + char *const argv[] = { (char *)bin_wait_pipe_sandbox, + pipe_child_str, pipe_parent_str, NULL }; + int ruleset_fd; + + /* Passes the pipe FDs to the executed binary. */ + EXPECT_EQ(0, close(pipe_child[0])); + EXPECT_EQ(0, close(pipe_parent[1])); + snprintf(pipe_child_str, sizeof(pipe_child_str), "%d", + pipe_child[1]); + snprintf(pipe_parent_str, sizeof(pipe_parent_str), "%d", + pipe_parent[0]); + + ruleset_fd = + landlock_create_ruleset(&layer1, sizeof(layer1), 0); + if (ruleset_fd < 0) { + perror("Failed to create a ruleset"); + _exit(1); + } + prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); + if (landlock_restrict_self(ruleset_fd, + variant->restrict_flags)) { + perror("Failed to restrict self"); + _exit(1); + } + close(ruleset_fd); + + ASSERT_EQ(0, execve(argv[0], argv, NULL)) + { + TH_LOG("Failed to execute \"%s\": %s", argv[0], + strerror(errno)); + }; + _exit(1); + return; + } + + EXPECT_EQ(0, close(pipe_child[1])); + EXPECT_EQ(0, close(pipe_parent[0])); + + /* Waits for the child. */ + EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + + /* Tests that there was no denial until now. */ + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + /* + * Wait for the child to do a first denied action by layer1 and + * sandbox itself with layer2. + */ + EXPECT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + + /* Tests that the audit record only matches the child. */ + if (variant->restrict_flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON) { + /* Matches the current domain. */ + EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, + getpid(), NULL)); + } + + /* Checks that we didn't miss anything. */ + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + + /* + * Wait for the child to do a second denied action by layer1 and + * layer2, and sandbox itself with layer3. + */ + EXPECT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + + /* Tests that the audit record only matches the child. */ + if (variant->restrict_flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON) { + /* Matches the current domain. */ + EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, + getpid(), NULL)); + } + + if (!(variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) { + /* Matches the child domain. */ + EXPECT_EQ(0, + matches_log_fs_read_root(_metadata, self->audit_fd)); + } + + /* Checks that we didn't miss anything. */ + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + + /* Waits for the child to terminate. */ + EXPECT_EQ(1, write(pipe_parent[1], ".", 1)); + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(1, WIFEXITED(status)); + ASSERT_EQ(0, WEXITSTATUS(status)); + + /* Tests that the audit record only matches the child. */ + if (!(variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) { + /* + * Matches the child domains, which tests that the + * llcred->domain_exec bitmask is correctly updated with a new + * domain. + */ + EXPECT_EQ(0, + matches_log_fs_read_root(_metadata, self->audit_fd)); + EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, + getpid(), NULL)); + } + + /* Checks that we didn't miss anything. */ + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h index fe6ada2c2518..c0a2d0b36217 100644 --- a/tools/testing/selftests/landlock/common.h +++ b/tools/testing/selftests/landlock/common.h @@ -31,6 +31,7 @@ static const char bin_sandbox_and_launch[] = "./sandbox-and-launch"; static const char bin_wait_pipe[] = "./wait-pipe"; +static const char bin_wait_pipe_sandbox[] = "./wait-pipe-sandbox"; static void _init_caps(struct __test_metadata *const _metadata, bool drop_all) { diff --git a/tools/testing/selftests/landlock/wait-pipe-sandbox.c b/tools/testing/selftests/landlock/wait-pipe-sandbox.c new file mode 100644 index 000000000000..87dbc9164430 --- /dev/null +++ b/tools/testing/selftests/landlock/wait-pipe-sandbox.c @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Write in a pipe, wait, sandbox itself, test sandboxing, and wait again. + * + * Used by audit_exec.flags from audit_test.c + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wrappers.h" + +static int sync_with(int pipe_child, int pipe_parent) +{ + char buf; + + /* Signals that we are waiting. */ + if (write(pipe_child, ".", 1) != 1) { + perror("Failed to write to first argument"); + return 1; + } + + /* Waits for the parent do its test. */ + if (read(pipe_parent, &buf, 1) != 1) { + perror("Failed to write to the second argument"); + return 1; + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + const struct landlock_ruleset_attr layer2 = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + }; + const struct landlock_ruleset_attr layer3 = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + int err, pipe_child, pipe_parent, ruleset_fd; + + /* The first argument must be the file descriptor number of a pipe. */ + if (argc != 3) { + fprintf(stderr, "Wrong number of arguments (not two)\n"); + return 1; + } + + pipe_child = atoi(argv[1]); + pipe_parent = atoi(argv[2]); + /* PR_SET_NO_NEW_PRIVS already set by parent. */ + + /* First step to test parent's layer1. */ + err = sync_with(pipe_child, pipe_parent); + if (err) + return err; + + /* Tries to send a signal, denied by layer1. */ + if (!kill(getppid(), 0)) { + fprintf(stderr, "Successfully sent a signal to the parent"); + return 1; + } + + /* Second step to test parent's layer1 and our layer2. */ + err = sync_with(pipe_child, pipe_parent); + if (err) + return err; + + ruleset_fd = landlock_create_ruleset(&layer2, sizeof(layer2), 0); + if (ruleset_fd < 0) { + perror("Failed to create the layer2 ruleset"); + return 1; + } + + if (landlock_restrict_self(ruleset_fd, 0)) { + perror("Failed to restrict self"); + return 1; + } + close(ruleset_fd); + + /* Tries to send a signal, denied by layer1. */ + if (!kill(getppid(), 0)) { + fprintf(stderr, "Successfully sent a signal to the parent"); + return 1; + } + + /* Tries to open ., denied by layer2. */ + if (open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) >= 0) { + fprintf(stderr, "Successfully opened /"); + return 1; + } + + /* Third step to test our layer2 and layer3. */ + err = sync_with(pipe_child, pipe_parent); + if (err) + return err; + + ruleset_fd = landlock_create_ruleset(&layer3, sizeof(layer3), 0); + if (ruleset_fd < 0) { + perror("Failed to create the layer3 ruleset"); + return 1; + } + + if (landlock_restrict_self(ruleset_fd, 0)) { + perror("Failed to restrict self"); + return 1; + } + close(ruleset_fd); + + /* Tries to open ., denied by layer2. */ + if (open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) >= 0) { + fprintf(stderr, "Successfully opened /"); + return 1; + } + + /* Tries to send a signal, denied by layer3. */ + if (!kill(getppid(), 0)) { + fprintf(stderr, "Successfully sent a signal to the parent"); + return 1; + } + + return 0; +} From patchwork Sat Mar 8 18:44:19 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007688 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-42ad.mail.infomaniak.ch (smtp-42ad.mail.infomaniak.ch [84.16.66.173]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6C6F72063C0 for ; Sat, 8 Mar 2025 18:45:09 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=84.16.66.173 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459511; cv=none; b=jXxoxYP9x9scs4tZ2vP+uRUKt4fvzFZ372sYSWcZHMzQH7DtmCU/GAmyaaCDtprPT88QIMD54g0bJtxEn0+C6mHKrGcAq3dEsMP6+rjR0mWI8bY4OLV12xCZvhKVWda/FeAKhq8NWsWFHzPo6i1iECMMzm8jFFwcA/HqE/lgI0Q= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459511; c=relaxed/simple; bh=kHEZn5MQ8LTT1IfzNKkZgFDj1sZfNbv6UBntPICu6W8=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=YDnvAf7WEcOde1mtv7bwu4PRUaGpjaidb4uTueS1p4myPACCsV2/m19yJuLo43U10vFDmUaoJkZ4KVzKxUWqseKd/tgam02XlHad/D0voIRbB70nyxY8H/lyvjsKA5LQgAMdjatJ0QERom4YP0S1DM1OfD8F5yMAupEnXemxqGc= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=mu+Zde5C; arc=none smtp.client-ip=84.16.66.173 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="mu+Zde5C" Received: from smtp-4-0001.mail.infomaniak.ch (unknown [IPv6:2001:1600:7:10:40ca:feff:fe05:1]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsv5873zSY9; Sat, 8 Mar 2025 19:45:07 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459507; bh=fvXrafkhzsfsp4waRL01f36DuQHCHVvFnVG4UZpORH0=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=mu+Zde5CrIrrMW+JtC8S9Ck5Kc3QOSipC/jx6TNTeyAswqEVw8ZyCiEWILQcD6tWC e8usI8CRzKa7hp2PFzl5HFAlvPJlDBCr2UbsOw/Ug9O8/WqfcwXOBNT6JbBvf2WINx kyVd4W0c3XMLzcoUb+ZylP43siQtjHjSpzHCE6sE= Received: from unknown by smtp-4-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bst63GwzjtJ; Sat, 8 Mar 2025 19:45:06 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 23/26] selftests/landlock: Add audit tests for ptrace Date: Sat, 8 Mar 2025 19:44:19 +0100 Message-ID: <20250308184422.2159360-24-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add tests for all ptrace actions checking "blockers=ptrace" records. This also improves PTRACE_TRACEME and PTRACE_ATTACH tests by making sure that the restrictions comes from Landlock, and with the expected process. These extended tests are like enhanced errno checks that make sure Landlock enforcement is consistent. Cc: Günther Noack Cc: Paul Moore Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-24-mic@digikod.net --- Changes since v5: - Move all audit tests to a new audit.trace test suite. - Simplify tests by only checking PTRACE_TRACEME and PTRACE_ATTACH with one scenario. This is preferable to not impact existing tests. - Make sure there is no unknown Landlock audit record. Changes since v3: - Update test coverage. Changes since v2: - New patch. --- .../testing/selftests/landlock/ptrace_test.c | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/tools/testing/selftests/landlock/ptrace_test.c b/tools/testing/selftests/landlock/ptrace_test.c index 8f31b673ff2d..eebd8268dccf 100644 --- a/tools/testing/selftests/landlock/ptrace_test.c +++ b/tools/testing/selftests/landlock/ptrace_test.c @@ -4,6 +4,7 @@ * * Copyright © 2017-2020 Mickaël Salaün * Copyright © 2019-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation */ #define _GNU_SOURCE @@ -17,6 +18,7 @@ #include #include +#include "audit.h" #include "common.h" /* Copied from security/yama/yama_lsm.c */ @@ -434,4 +436,142 @@ TEST_F(hierarchy, trace) _metadata->exit_code = KSFT_FAIL; } +static int matches_log_ptrace(struct __test_metadata *const _metadata, + int audit_fd, const pid_t opid) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " blockers=ptrace opid=%d ocomm=\"ptrace_test\"$"; + char log_match[sizeof(log_template) + 10]; + int log_match_len; + + log_match_len = + snprintf(log_match, sizeof(log_match), log_template, opid); + if (log_match_len > sizeof(log_match)) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match, + NULL); +} + +FIXTURE(audit) +{ + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_SETUP(audit) +{ + disable_caps(_metadata); + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + EXPECT_LE(0, self->audit_fd); + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +FIXTURE_TEARDOWN_PARENT(audit) +{ + EXPECT_EQ(0, audit_cleanup(-1, NULL)); +} + +/* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */ +TEST_F(audit, trace) +{ + pid_t child, parent; + int status; + int pipe_child[2], pipe_parent[2]; + int yama_ptrace_scope; + char buf_parent; + struct audit_records records; + + /* Makes sure there is no superfluous logged records. */ + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + yama_ptrace_scope = get_yama_ptrace_scope(); + ASSERT_LE(0, yama_ptrace_scope); + + if (yama_ptrace_scope > YAMA_SCOPE_DISABLED) + TH_LOG("Incomplete tests due to Yama restrictions (scope %d)", + yama_ptrace_scope); + + /* + * Removes all effective and permitted capabilities to not interfere + * with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS. + */ + drop_caps(_metadata); + + parent = getpid(); + ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + char buf_child; + + ASSERT_EQ(0, close(pipe_parent[1])); + ASSERT_EQ(0, close(pipe_child[0])); + + /* Waits for the parent to be in a domain, if any. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); + + /* Tests child PTRACE_TRACEME. */ + EXPECT_EQ(-1, ptrace(PTRACE_TRACEME)); + EXPECT_EQ(EPERM, errno); + /* We should indeed see the parent process. */ + matches_log_ptrace(_metadata, self->audit_fd, parent); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + /* Checks for a domain creation. */ + EXPECT_EQ(1, records.domain); + + /* + * Signals that the PTRACE_ATTACH test is done and the + * PTRACE_TRACEME test is ongoing. + */ + ASSERT_EQ(1, write(pipe_child[1], ".", 1)); + + /* Waits for the parent PTRACE_ATTACH test. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); + _exit(_metadata->exit_code); + return; + } + + ASSERT_EQ(0, close(pipe_child[1])); + ASSERT_EQ(0, close(pipe_parent[0])); + create_domain(_metadata); + + /* Signals that the parent is in a domain. */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + + /* + * Waits for the child to test PTRACE_ATTACH on the parent and start + * testing PTRACE_TRACEME. + */ + ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + + /* The child should not be traced by the parent. */ + EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0)); + EXPECT_EQ(ESRCH, errno); + + /* Tests PTRACE_ATTACH on the child. */ + EXPECT_EQ(-1, ptrace(PTRACE_ATTACH, child, NULL, 0)); + EXPECT_EQ(EPERM, errno); + EXPECT_EQ(0, matches_log_ptrace(_metadata, self->audit_fd, child)); + + /* Signals that the parent PTRACE_ATTACH test is done. */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; + + /* Makes sure there is no superfluous logged records. */ + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + TEST_HARNESS_MAIN From patchwork Sat Mar 8 18:44:20 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007689 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-bc0c.mail.infomaniak.ch (smtp-bc0c.mail.infomaniak.ch [45.157.188.12]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id EECF22192F0 for ; Sat, 8 Mar 2025 18:45:10 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=45.157.188.12 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459513; cv=none; b=VwXOt/JB8xlQoXkUx4lfVYy6yFod1aQxj0NxqZkQlYldf3ony3vvfs44+0+5lm9P5x32bB9QZ9ekhyur1vdByiP5d/IcGNSkCqFuF/3liztcCAZ89dlfPjPxDBU2YVroj8wfA0o0sQb+4+ityJfUwiWjm/5BswosywYiJ0pm6pI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459513; c=relaxed/simple; bh=XRbz7lvEQY0aNuJMptZCxp+4ZgmpF8XcathU82NSabQ=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=ROTiJqv2BkE46uHb8R+99yGWt3YPUKLKPm0dI7qFkMQWNmorR4k9ZZFSWdO+FqylHOvkbLyRzmezRKlxqeIrb1Bz3DJMD+xNBliRNg6G5UMEmiANUxGi4xSabhdfVMK5UAA15Ovj/T8Lb50rSfkfkNnc+liG2lMLLs7pB2kSHtg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=P+9Yh/pw; arc=none smtp.client-ip=45.157.188.12 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="P+9Yh/pw" Received: from smtp-3-0000.mail.infomaniak.ch (unknown [IPv6:2001:1600:4:17::246b]) by smtp-3-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsx1cJLzNqX; Sat, 8 Mar 2025 19:45:09 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459509; bh=xJtlNKGnSbmGPejBcDimlJU9c0REBHXBhUsr8iKogCM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=P+9Yh/pwzfqGJxdn6sg+PeZ6o9WQDuP1Vn1nUpCEyMLGddGB4rlMT29IeHvFehmdK CsqidCIo1aJMm/+Ff0GBY8f5CIpkaWqcshIY9KmaHUlppoDpdgvIm5crqcQAyZn/Vx AVEWNkyEf/ElHq4JWBUH+AgpZy/s0y+5eqBNSk2w= Received: from unknown by smtp-3-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsw3Jg6zqjd; Sat, 8 Mar 2025 19:45:08 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 24/26] selftests/landlock: Add audit tests for abstract unix socket scoping Date: Sat, 8 Mar 2025 19:44:20 +0100 Message-ID: <20250308184422.2159360-25-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Add a new scoped_uadit.connect_to_child test to check the abstract unix socket blocker. Cc: Günther Noack Cc: Paul Moore Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-25-mic@digikod.net --- Changes since v5: - New patch. --- .../landlock/scoped_abstract_unix_test.c | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c index a6b59d2ab1b4..520b90d5f2f9 100644 --- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c +++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c @@ -20,6 +20,7 @@ #include #include +#include "audit.h" #include "common.h" #include "scoped_common.h" @@ -267,6 +268,126 @@ TEST_F(scoped_domains, connect_to_child) _metadata->exit_code = KSFT_FAIL; } +FIXTURE(scoped_audit) +{ + struct service_fixture dgram_address; + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_SETUP(scoped_audit) +{ + disable_caps(_metadata); + + memset(&self->dgram_address, 0, sizeof(self->dgram_address)); + set_unix_address(&self->dgram_address, 1); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + EXPECT_LE(0, self->audit_fd); + drop_caps(_metadata); +} + +FIXTURE_TEARDOWN_PARENT(scoped_audit) +{ + EXPECT_EQ(0, audit_cleanup(-1, NULL)); +} + +/* python -c 'print(b"\0selftests-landlock-abstract-unix-".hex().upper())' */ +#define ABSTRACT_SOCKET_PATH_PREFIX \ + "0073656C6674657374732D6C616E646C6F636B2D61627374726163742D756E69782D" + +static int matches_log_abstract_socket(struct __test_metadata *const _metadata, + int audit_fd) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX + "[0-9A-F]\\+$"; + char log_match[sizeof(log_template) + 10]; + int log_match_len; + + log_match_len = snprintf(log_match, sizeof(log_match), log_template); + if (log_match_len > sizeof(log_match)) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match, + NULL); +} + +/* + * Simpler version of scoped_domains.connect_to_child, but with audit tests. + */ +TEST_F(scoped_audit, connect_to_child) +{ + pid_t child; + int err_dgram, status; + int pipe_child[2], pipe_parent[2]; + char buf; + int dgram_client; + struct audit_records records; + + /* Makes sure there is no superfluous logged records. */ + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + int dgram_server; + + EXPECT_EQ(0, close(pipe_parent[1])); + EXPECT_EQ(0, close(pipe_child[0])); + + /* Waits for the parent to be in a domain. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); + + dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_server); + ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr, + self->dgram_address.unix_addr_len)); + + /* Signals to the parent that child is listening. */ + ASSERT_EQ(1, write(pipe_child[1], ".", 1)); + + /* Waits to connect. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); + EXPECT_EQ(0, close(dgram_server)); + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(pipe_child[1])); + EXPECT_EQ(0, close(pipe_parent[0])); + + create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + /* Signals that the parent is in a domain, if any. */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + + dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_client); + + /* Waits for the child to listen */ + ASSERT_EQ(1, read(pipe_child[0], &buf, 1)); + err_dgram = connect(dgram_client, &self->dgram_address.unix_addr, + self->dgram_address.unix_addr_len); + EXPECT_EQ(-1, err_dgram); + EXPECT_EQ(EPERM, errno); + + EXPECT_EQ(0, matches_log_abstract_socket(_metadata, self->audit_fd)); + + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(0, close(dgram_client)); + + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + FIXTURE(scoped_vs_unscoped) { struct service_fixture parent_stream_address, parent_dgram_address, From patchwork Sat Mar 8 18:44:21 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007690 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-190d.mail.infomaniak.ch (smtp-190d.mail.infomaniak.ch [185.125.25.13]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id DBA5C219A74 for ; Sat, 8 Mar 2025 18:45:12 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.125.25.13 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459515; cv=none; b=T8FggC9wQldBsl1JhqGMviGnQ/C8W5q1v81WcmkCkTUjss53BU1+RhaenpxUD0e4tvC/H9dedknXg5E8JhcYeZPL8Mj1y44l+BUcCa4D+XuNmJCE7Mg3Z/PESwPF7is1J+bL0c66P0hAcB1vqM+L5v0/b7rUSyBoWYOWzLLgXIE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459515; c=relaxed/simple; bh=9LDxuRU8reoAJoy4A38nJFBJq/kT7oF+bhSaK4+C/3w=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=NBiFxSuj5wgiFm2InLYRINYo4FJZOzGgsiXDfzyH+G/wxjEa695JfUZ2ITsmVu/hOMyWp0lgkwaAQif8W11PNlPxs9tfleKgPR7gqJJTfbx+cRaODEiXIJ0SUOsnD33YTq7+c7VIedeZ0i2dIMso+WGvd98YllvZFhBRN6ykSy8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=cfPOoDAr; arc=none smtp.client-ip=185.125.25.13 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="cfPOoDAr" Received: from smtp-4-0000.mail.infomaniak.ch (unknown [IPv6:2001:1600:7:10:40ca:feff:fe05:0]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bsz22xdzShN; Sat, 8 Mar 2025 19:45:11 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459511; bh=7CG5muF+gwo8MsmFi5DT4xu/AZ64anUvZy1IoiAepOc=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=cfPOoDArctk1RV+L8y67FHJvVoHHaqH9+Ey0scTBc7tAtzs59TxS/ixKZ2KcrOV8V KeZA5VvXYEV3lkm2NvjjcGIP+Ceb3aGqWYMTYPOJUGsTztaMTWiS77H7KV1xDGzP3h ha2X1ELgarkqUmt0lgeozSEynopXUKKnxgqSqW1c= Received: from unknown by smtp-4-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsy0DfWzKf2; Sat, 8 Mar 2025 19:45:10 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 25/26] selftests/landlock: Add audit tests for filesystem Date: Sat, 8 Mar 2025 19:44:21 +0100 Message-ID: <20250308184422.2159360-26-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Test all filesystem blockers, including events with several records, and record with several blockers: - fs.execute - fs.write_file - fs.read_file - fs_read_dir - fs.remove_dir - fs.remove_file - fs.make_char - fs.make_dir - fs.make_reg - fs.make_sock - fs.make_fifo - fs.make_block - fs.make_sym - fs.refer - fs.truncate - fs.ioctl_dev Test coverage for security/landlock is 93.4% of 1407 lines according to gcc/gcov-14. Cc: Günther Noack Cc: Paul Moore Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-26-mic@digikod.net --- Changes since v5: - New patch. --- tools/testing/selftests/landlock/audit.h | 39 ++ tools/testing/selftests/landlock/common.h | 16 + tools/testing/selftests/landlock/fs_test.c | 455 +++++++++++++++++++++ 3 files changed, 510 insertions(+) diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h index b3b3e52e8256..d84a88ff6535 100644 --- a/tools/testing/selftests/landlock/audit.h +++ b/tools/testing/selftests/landlock/audit.h @@ -22,6 +22,10 @@ #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) #endif +#ifndef __maybe_unused +#define __maybe_unused __attribute__((__unused__)) +#endif + #define REGEX_LANDLOCK_PREFIX "^audit([0-9.:]\\+): domain=\\([0-9a-f]\\+\\)" struct audit_filter { @@ -188,6 +192,41 @@ static int audit_set_status(int fd, __u32 key, __u32 val) return audit_request(fd, &msg, NULL); } +/* Returns a pointer to the last filled character of @dst, which is `\0`. */ +static __maybe_unused char *regex_escape(const char *const src, char *dst, + size_t dst_size) +{ + char *d = dst; + + for (const char *s = src; *s; s++) { + switch (*s) { + case '$': + case '*': + case '.': + case '[': + case '\\': + case ']': + case '^': + if (d >= dst + dst_size - 2) + return (char *)-ENOMEM; + + *d++ = '\\'; + *d++ = *s; + break; + default: + if (d >= dst + dst_size - 1) + return (char *)-ENOMEM; + + *d++ = *s; + } + } + if (d >= dst + dst_size - 1) + return (char *)-ENOMEM; + + *d = '\0'; + return d; +} + /* * @domain_id: The domain ID extracted from the audit message (if the first part * of @pattern is REGEX_LANDLOCK_PREFIX). It is set to 0 if the domain ID is diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h index c0a2d0b36217..ddcd8d00aeb3 100644 --- a/tools/testing/selftests/landlock/common.h +++ b/tools/testing/selftests/landlock/common.h @@ -207,6 +207,22 @@ enforce_ruleset(struct __test_metadata *const _metadata, const int ruleset_fd) } } +static void __maybe_unused +drop_access_rights(struct __test_metadata *const _metadata, + const struct landlock_ruleset_attr *const ruleset_attr) +{ + int ruleset_fd; + + ruleset_fd = + landlock_create_ruleset(ruleset_attr, sizeof(*ruleset_attr), 0); + EXPECT_LE(0, ruleset_fd) + { + TH_LOG("Failed to create a ruleset: %s", strerror(errno)); + } + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); +} + struct protocol_variant { int domain; int type; diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c index aa6f2c1cbec7..873d38efdad7 100644 --- a/tools/testing/selftests/landlock/fs_test.c +++ b/tools/testing/selftests/landlock/fs_test.c @@ -41,6 +41,7 @@ #define _ASM_GENERIC_FCNTL_H #include +#include "audit.h" #include "common.h" #ifndef renameat2 @@ -5554,4 +5555,458 @@ TEST_F_FORK(layout3_fs, release_inodes) ASSERT_EQ(EACCES, test_open(TMP_DIR, O_RDONLY)); } +static int matches_log_fs_extra(struct __test_metadata *const _metadata, + int audit_fd, const char *const blockers, + const char *const path, const char *const extra) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " blockers=fs\\.%s path=\"%s\" dev=\"[^\"]\\+\" ino=[0-9]\\+$"; + char *absolute_path = NULL; + size_t log_match_remaining = sizeof(log_template) + strlen(blockers) + + PATH_MAX * 2 + + (extra ? strlen(extra) : 0) + 1; + char log_match[log_match_remaining]; + char *log_match_cursor = log_match; + size_t chunk_len; + + chunk_len = snprintf(log_match_cursor, log_match_remaining, + REGEX_LANDLOCK_PREFIX " blockers=%s path=\"", + blockers); + if (chunk_len < 0 || chunk_len >= log_match_remaining) + return -E2BIG; + + absolute_path = realpath(path, NULL); + if (!absolute_path) + return -errno; + + log_match_remaining -= chunk_len; + log_match_cursor += chunk_len; + log_match_cursor = regex_escape(absolute_path, log_match_cursor, + log_match_remaining); + free(absolute_path); + if (log_match_cursor < 0) + return (long long)log_match_cursor; + + log_match_remaining -= log_match_cursor - log_match; + chunk_len = snprintf(log_match_cursor, log_match_remaining, + "\" dev=\"[^\"]\\+\" ino=[0-9]\\+%s$", + extra ?: ""); + if (chunk_len < 0 || chunk_len >= log_match_remaining) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match, + NULL); +} + +static int matches_log_fs(struct __test_metadata *const _metadata, int audit_fd, + const char *const blockers, const char *const path) +{ + return matches_log_fs_extra(_metadata, audit_fd, blockers, path, NULL); +} + +FIXTURE(audit_layout1) +{ + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_SETUP(audit_layout1) +{ + prepare_layout(_metadata); + + create_layout1(_metadata); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + EXPECT_LE(0, self->audit_fd); + drop_caps(_metadata); +} + +FIXTURE_TEARDOWN_PARENT(audit_layout1) +{ + remove_layout1(_metadata); + + cleanup_layout(_metadata); + + EXPECT_EQ(0, audit_cleanup(-1, NULL)); +} + +TEST_F(audit_layout1, execute) +{ + struct audit_records records; + + copy_file(_metadata, bin_true, file1_s1d1); + test_execute(_metadata, 0, file1_s1d1); + test_check_exec(_metadata, 0, file1_s1d1); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_EXECUTE, + }); + + test_execute(_metadata, EACCES, file1_s1d1); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.execute", + file1_s1d1)); + + test_check_exec(_metadata, EACCES, file1_s1d1); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.execute", + file1_s1d1)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +TEST_F(audit_layout1, write_file) +{ + struct audit_records records; + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_WRITE_FILE, + }); + + EXPECT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.write_file", file1_s1d1)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, read_file) +{ + struct audit_records records; + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_READ_FILE, + }); + + EXPECT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.read_file", + file1_s1d1)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, read_dir) +{ + struct audit_records records; + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_READ_DIR, + }); + + EXPECT_EQ(EACCES, test_open(dir_s1d1, O_DIRECTORY)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.read_dir", + dir_s1d1)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, remove_dir) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + EXPECT_EQ(0, unlink(file2_s1d3)); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_REMOVE_DIR, + }); + + EXPECT_EQ(-1, rmdir(dir_s1d3)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_dir", dir_s1d2)); + + EXPECT_EQ(-1, unlinkat(AT_FDCWD, dir_s1d3, AT_REMOVEDIR)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_dir", dir_s1d2)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +TEST_F(audit_layout1, remove_file) +{ + struct audit_records records; + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_REMOVE_FILE, + }); + + EXPECT_EQ(-1, unlink(file1_s1d3)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file", dir_s1d3)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_char) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_MAKE_CHAR, + }); + + EXPECT_EQ(-1, mknod(file1_s1d3, S_IFCHR | 0644, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_char", + dir_s1d3)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_dir) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_MAKE_DIR, + }); + + EXPECT_EQ(-1, mkdir(file1_s1d3, 0755)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_dir", + dir_s1d3)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_reg) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_MAKE_REG, + }); + + EXPECT_EQ(-1, mknod(file1_s1d3, S_IFREG | 0644, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_reg", + dir_s1d3)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_sock) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_MAKE_SOCK, + }); + + EXPECT_EQ(-1, mknod(file1_s1d3, S_IFSOCK | 0644, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_sock", + dir_s1d3)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_fifo) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_MAKE_FIFO, + }); + + EXPECT_EQ(-1, mknod(file1_s1d3, S_IFIFO | 0644, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_fifo", + dir_s1d3)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_block) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_MAKE_BLOCK, + }); + + EXPECT_EQ(-1, mknod(file1_s1d3, S_IFBLK | 0644, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.make_block", dir_s1d3)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_sym) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_MAKE_SYM, + }); + + EXPECT_EQ(-1, symlink("target", file1_s1d3)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_sym", + dir_s1d3)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +// TODO: Add refer_exchange +TEST_F(audit_layout1, refer_handled) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_REFER, + }); + + EXPECT_EQ(-1, link(file1_s1d1, file1_s1d3)); + EXPECT_EQ(EXDEV, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s1d1)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s1d3)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +TEST_F(audit_layout1, refer_make) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REFER, + }); + + EXPECT_EQ(-1, link(file1_s1d1, file1_s1d3)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s1d1)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.make_reg,fs\\.refer", dir_s1d3)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +TEST_F(audit_layout1, truncate) +{ + struct audit_records records; + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_TRUNCATE, + }); + + EXPECT_EQ(-1, truncate(file1_s1d3, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.truncate", + file1_s1d3)); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, ioctl_dev) +{ + struct audit_records records; + int fd; + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_IOCTL_DEV, + }); + + fd = open("/dev/null", O_RDWR | O_CLOEXEC); + ASSERT_LE(0, fd); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FIONREAD)); + EXPECT_EQ(0, matches_log_fs_extra(_metadata, self->audit_fd, + "fs\\.ioctl_dev", "/dev/null", + " ioctlcmd=0x541b")); + + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + TEST_HARNESS_MAIN From patchwork Sat Mar 8 18:44:22 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 14007691 X-Patchwork-Delegate: paul@paul-moore.com Received: from smtp-190f.mail.infomaniak.ch (smtp-190f.mail.infomaniak.ch [185.125.25.15]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9E00C21ADCE for ; Sat, 8 Mar 2025 18:45:14 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.125.25.15 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459518; cv=none; b=nV6rYiIUUX4X7Ag7JHwabF24Q5rfUm4o4V1jjmiDp13TFNVYZMXph1y7bCz0k7qG5ihdSpIGyZz5ALpgTsWVP1Ob3fBwTrVHM8y5IAGdZzK/lqjCIkBqJZuJjlS1vhnyD+3rCfofMvoIEN9BZVmPMF0EWX9a091ezjQCaWSNQGk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1741459518; c=relaxed/simple; bh=065DKDG4l8GjBe6nTlgibA+hgQmFugtxUREoPraYyEQ=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=rXzyqFYbuHEtaqFZmLrPEwASBpq/gv7jZWkFX+ppSgJ403QoKOAYomLZZ7INzRO5+JQ1YddOUlreWAPZHuc65C7pD54W6CpYuTRdj+HPw4oCEUzfQf+fPUstuXFNXM5y+mhQNkUGSf9/MTKqcXaryw3x46gsFbk6zR2lp5b8mNU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b=dNbCiEVA; arc=none smtp.client-ip=185.125.25.15 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=digikod.net Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=digikod.net header.i=@digikod.net header.b="dNbCiEVA" Received: from smtp-4-0000.mail.infomaniak.ch (unknown [IPv6:2001:1600:7:10:40ca:feff:fe05:0]) by smtp-4-3000.mail.infomaniak.ch (Postfix) with ESMTPS id 4Z9Bt067zlzQ9D; Sat, 8 Mar 2025 19:45:12 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1741459512; bh=rbxAVM0frrhyQfoNxBqPXz6ATnPq5nEhIbUQovrqcuo=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=dNbCiEVANvyiw/MbQa7PWF8ylPOv8niePSHUQKhR7vhSidXWcHXvrZoUPPe6aFl/A lZOimP9LOe7B6UEKhJRX6SV09AYk2s+KuH/CUHvA7ADhz1w3l/+pGanXUvhxCnWMuE vugBlE56oY+7fI0GKRb3iKhIVspqxf++CELlzjlI= Received: from unknown by smtp-4-0000.mail.infomaniak.ch (Postfix) with ESMTPA id 4Z9Bsz6K8SzHMx; Sat, 8 Mar 2025 19:45:11 +0100 (CET) From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: Eric Paris , Paul Moore , =?utf-8?q?G=C3=BCnther_Noack?= , "Serge E . Hallyn" Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Ben Scarlato , Casey Schaufler , Charles Zaffery , Daniel Burgener , Francis Laniel , James Morris , Jann Horn , Jeff Xu , Jorge Lucangeli Obes , Kees Cook , Konstantin Meskhidze , Matt Bobrowski , Mikhail Ivanov , Phil Sutter , Praveen K Paladugu , Robert Salvet , Shervin Oloumi , Song Liu , Tahera Fahimi , Tingmao Wang , Tyler Hicks , audit@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org Subject: [PATCH v6 26/26] landlock: Add audit documentation Date: Sat, 8 Mar 2025 19:44:22 +0100 Message-ID: <20250308184422.2159360-27-mic@digikod.net> In-Reply-To: <20250308184422.2159360-1-mic@digikod.net> References: <20250308184422.2159360-1-mic@digikod.net> Precedence: bulk X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Infomaniak-Routing: alpha Because audit is dedicated to the system administrator, create a new entry in Documentation/admin-guide/LSM . Extend other Landlock documentation's pages with this new one. Extend the guiding principles with logs. Cc: Günther Noack Cc: Paul Moore Signed-off-by: Mickaël Salaün Link: https://lore.kernel.org/r/20250308184422.2159360-27-mic@digikod.net --- Changes since v5: - Extend the guiding principles with logs. Changes since v4: - New patch. --- Documentation/admin-guide/LSM/index.rst | 1 + Documentation/admin-guide/LSM/landlock.rst | 158 +++++++++++++++++++++ Documentation/security/landlock.rst | 13 +- Documentation/userspace-api/landlock.rst | 9 +- MAINTAINERS | 1 + 5 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 Documentation/admin-guide/LSM/landlock.rst diff --git a/Documentation/admin-guide/LSM/index.rst b/Documentation/admin-guide/LSM/index.rst index ce63be6d64ad..b44ef68f6e4d 100644 --- a/Documentation/admin-guide/LSM/index.rst +++ b/Documentation/admin-guide/LSM/index.rst @@ -48,3 +48,4 @@ subdirectories. Yama SafeSetID ipe + landlock diff --git a/Documentation/admin-guide/LSM/landlock.rst b/Documentation/admin-guide/LSM/landlock.rst new file mode 100644 index 000000000000..9e61607def08 --- /dev/null +++ b/Documentation/admin-guide/LSM/landlock.rst @@ -0,0 +1,158 @@ +.. SPDX-License-Identifier: GPL-2.0 +.. Copyright © 2025 Microsoft Corporation + +================================ +Landlock: system-wide management +================================ + +:Author: Mickaël Salaün +:Date: March 2025 + +Landlock can leverage the audit framework to log events. + +User space documentation can be found here: +Documentation/userspace-api/landlock.rst. + +Audit +===== + +Denied access requests are logged by default for a sandboxed program if `audit` +is enabled. This default behavior can be changed with the +sys_landlock_restrict_self() flags (cf. +Documentation/userspace-api/landlock.rst). Landlock logs can also be masked +thanks to audit rules. Landlock can generate 2 audit record types. + +Record types +------------ + +AUDIT_LANDLOCK_ACCESS + This record type identifies a denied access request to a kernel resource. + The ``domain`` field indicates the ID of the domain that blocked the + request. The ``blockers`` field indicates the cause(s) of this denial + (separated by a comma), and the following fields identify the kernel object + (similar to SELinux). There may be more than one of this record type per + audit event. + + Example with a file link request generating two records in the same event:: + + domain=195ba459b blockers=fs.refer path="/usr/bin" dev="vda2" ino=351 + domain=195ba459b blockers=fs.make_reg,fs.refer path="/usr/local" dev="vda2" ino=365 + +AUDIT_LANDLOCK_DOMAIN + This record type describes the status of a Landlock domain. The ``status`` + field can be either ``allocated`` or ``deallocated``. + + The ``allocated`` status is part of the same audit event and follows + the first logged ``AUDIT_LANDLOCK_ACCESS`` record of a domain. It identifies + Landlock domain information at the time of the sys_landlock_restrict_self() + call with the following fields: + + - the ``domain`` ID + - the enforcement ``mode`` + - the domain creator's ``pid`` + - the domain creator's ``uid`` + - the domain creator's executable path (``exe``) + - the domain creator's command line (``comm``) + + Example:: + + domain=195ba459b status=allocated mode=enforcing pid=300 uid=0 exe="/root/sandboxer" comm="sandboxer" + + The ``deallocated`` status is an event on its own and it identifies a + Landlock domain release. After such event, it is guarantee that the + related domain ID will never be reused during the lifetime of the system. + The ``domain`` field indicates the ID of the domain which is released, and + the ``denials`` field indicates the total number of denied access request, + which might not have been logged according to the audit rules and + sys_landlock_restrict_self()'s flags. + + Example:: + + domain=195ba459b status=deallocated denials=3 + + +Event samples +-------------- + +Here are two examples of log events (see serial numbers). + +In this example a sandboxed program (``kill``) tries to send a signal to the +init process, which is denied because of the signal scoping restriction +(``LL_SCOPED=s``):: + + $ LL_FS_RO=/ LL_FS_RW=/ LL_SCOPED=s LL_FORCE_LOG=1 ./sandboxer kill 1 + +This command generates two events, each identified with a unique serial +number following a timestamp (``msg=audit(1729738800.268:30)``). The first +event (serial ``30``) contains 4 records. The first record +(``type=LANDLOCK_ACCESS``) shows an access denied by the domain `1a6fdc66f`. +The cause of this denial is signal scopping restriction +(``blockers=scope.signal``). The process that would have receive this signal +is the init process (``opid=1 ocomm="systemd"``). + +The second record (``type=LANDLOCK_DOMAIN``) describes (``status=allocated``) +domain `1a6fdc66f`. This domain was created by process ``286`` executing the +``/root/sandboxer`` program launched by the root user. + +The third record (``type=SYSCALL``) describes the syscall, its provided +arguments, its result (``success=no exit=-1``), and the process that called it. + +The fourth record (``type=PROCTITLE``) shows the command's name as an +hexadecimal value. This can be translated with ``python -c +'print(bytes.fromhex("6B696C6C0031"))'``. + +Finally, the last record (``type=LANDLOCK_DOMAIN``) is also the only one from +the second event (serial ``31``). It is not tied to a direct user space action +but an asynchronous one to free resources tied to a Landlock domain +(``status=deallocated``). This can be useful to know that the following logs +will not concern the domain ``1a6fdc66f`` anymore. This record also summarize +the number of requests this domain denied (``denials=1``), whether they were +logged or not. + +.. code-block:: + + type=LANDLOCK_ACCESS msg=audit(1729738800.268:30): domain=1a6fdc66f blockers=scope.signal opid=1 ocomm="systemd" + type=LANDLOCK_DOMAIN msg=audit(1729738800.268:30): domain=1a6fdc66f status=allocated mode=enforcing pid=286 uid=0 exe="/root/sandboxer" comm="sandboxer" + type=SYSCALL msg=audit(1729738800.268:30): arch=c000003e syscall=62 success=no exit=-1 [..] ppid=272 pid=286 auid=0 uid=0 gid=0 [...] comm="kill" [...] + type=PROCTITLE msg=audit(1729738800.268:30): proctitle=6B696C6C0031 + type=LANDLOCK_DOMAIN msg=audit(1729738800.324:31): domain=1a6fdc66f status=deallocated denials=1 + +Here is another example showcasing filesystem access control:: + + $ LL_FS_RO=/ LL_FS_RW=/tmp LL_FORCE_LOG=1 ./sandboxer sh -c "echo > /etc/passwd" + +The related audit logs contains 8 records from 3 different events (serials 33, +34 and 35) created by the same domain `1a6fdc679`:: + + type=LANDLOCK_ACCESS msg=audit(1729738800.221:33): domain=1a6fdc679 blockers=fs.write_file path="/dev/tty" dev="devtmpfs" ino=9 + type=LANDLOCK_DOMAIN msg=audit(1729738800.221:33): domain=1a6fdc679 status=allocated mode=enforcing pid=289 uid=0 exe="/root/sandboxer" comm="sandboxer" + type=SYSCALL msg=audit(1729738800.221:33): arch=c000003e syscall=257 success=no exit=-13 [...] ppid=272 pid=289 auid=0 uid=0 gid=0 [...] comm="sh" [...] + type=PROCTITLE msg=audit(1729738800.221:33): proctitle=7368002D63006563686F203E202F6574632F706173737764 + type=LANDLOCK_ACCESS msg=audit(1729738800.221:34): domain=1a6fdc679 blockers=fs.write_file path="/etc/passwd" dev="vda2" ino=143821 + type=SYSCALL msg=audit(1729738800.221:34): arch=c000003e syscall=257 success=no exit=-13 [...] ppid=272 pid=289 auid=0 uid=0 gid=0 [...] comm="sh" [...] + type=PROCTITLE msg=audit(1729738800.221:34): proctitle=7368002D63006563686F203E202F6574632F706173737764 + type=LANDLOCK_DOMAIN msg=audit(1729738800.261:35): domain=1a6fdc679 status=deallocated denials=2 + + +Event filtering +--------------- + +If you get spammed with audit logs related to Landlock, this is either an +attack attempt or a bug in the security policy. We can put in place some +filters to limit noise with two complementary ways: + +- with sys_landlock_restrict_self()'s flags if we can fix the sandboxed + programs, +- or with audit rules (see :manpage:`auditctl(8)`). + +Additional documentation +======================== + +* `Linux Audit Documentation`_ +* Documentation/userspace-api/landlock.rst +* Documentation/security/landlock.rst +* https://landlock.io + +.. Links +.. _Linux Audit Documentation: + https://github.com/linux-audit/audit-documentation/wiki diff --git a/Documentation/security/landlock.rst b/Documentation/security/landlock.rst index 59ecdb1c0d4d..e0fc54aff09e 100644 --- a/Documentation/security/landlock.rst +++ b/Documentation/security/landlock.rst @@ -7,7 +7,7 @@ Landlock LSM: kernel documentation ================================== :Author: Mickaël Salaün -:Date: December 2022 +:Date: March 2025 Landlock's goal is to create scoped access-control (i.e. sandboxing). To harden a whole system, this feature should be available to any process, @@ -45,6 +45,10 @@ Guiding principles for safe access controls sandboxed process shall retain their scoped accesses (at the time of resource acquisition) whatever process uses them. Cf. `File descriptor access rights`_. +* Access denials shall be logged according to system and Landlock domain + configurations. Log entries must contain information about the cause of the + denial and the owner of the related security policy. Such log generation + should have a negligible performance and memory impact on allowed requests. Design choices ============== @@ -124,6 +128,13 @@ makes the reasoning much easier and helps avoid pitfalls. .. kernel-doc:: security/landlock/ruleset.h :identifiers: +Additional documentation +======================== + +* Documentation/userspace-api/landlock.rst +* Documentation/admin-guide/LSM/landlock.rst +* https://landlock.io + .. Links .. _tools/testing/selftests/landlock/: https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/tools/testing/selftests/landlock/ diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst index ad587f53fe41..2a9843116202 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: January 2025 +:Date: March 2025 The goal of Landlock is to enable restriction of ambient rights (e.g. global filesystem or network access) for a set of processes. Because Landlock @@ -683,9 +683,16 @@ fine-grained restrictions). Moreover, their complexity can lead to security issues, especially when untrusted processes can manipulate them (cf. `Controlling access to user namespaces `_). +How to disable Landlock audit records? +-------------------------------------- + +You might want to put in place filters as explained here: +Documentation/admin-guide/LSM/landlock.rst + Additional documentation ======================== +* Documentation/admin-guide/LSM/landlock.rst * Documentation/security/landlock.rst * https://landlock.io diff --git a/MAINTAINERS b/MAINTAINERS index 8e0736dc2ee0..a3aa52e47401 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -13075,6 +13075,7 @@ L: linux-security-module@vger.kernel.org S: Supported W: https://landlock.io T: git https://git.kernel.org/pub/scm/linux/kernel/git/mic/linux.git +F: Documentation/admin-guide/LSM/landlock.rst F: Documentation/security/landlock.rst F: Documentation/userspace-api/landlock.rst F: fs/ioctl.c