From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=45.157.188.12 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1794342659; b=Z2VF+Go98h4egp8nDxkqIoZ0g3wgaBCCG+HOpmeglTWsf1wmPqIQcYCC+eJkCcZiAzrD 6iDiaQAWCx1b5vMwkPVYe8Xy5Fr7UAR6NfYOVqRedSam7NbyQ3/hx1CcE5gTabwJXpdXU WEOHm1Cgy0TBUxQN2n6DoedCouBelwQWv8r0eKckum7zAqtpRdXS5BomRBH/KakE9sEbF TpglKPis0IxZcj6K7DnzFiuVsyjq+IJDex082jHEBDqUkxPolt6d2YW3hlfQ2jLJJmjqz 3UbntNvaGKPRZqvSR6Gr3p1p/6/EEJPWKscCr4NgcX1uCFcwUCnohRkV8b7n9Qr9Oew== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1794342659; h=DMARC-Filter:Received:DKIM-Signature:Received:From:To:Cc:Subject: Date:Message-ID:In-Reply-To:References:MIME-Version:Content-Type: Content-Transfer-Encoding:X-Infomaniak-Routing; bh=gfuupJbALZD+q1BFqL8+mKjX6fPsKBJ1ASODJI4cCsU=; b=btcAzzQXxSu3/9+YglnSDrfgyM0j+ap5MQ8S0jcANMEJuSKN+ZlehLdsSmTvFjZfcJaV WePOqyQNK7zVjNJx7at8qnxBoX45EqLV2yWCZSMBIe/tRfyx3Lo4+NjgpAPgRgch1/cM0 ExeCauCQs9lur2OwW9MFpW0yv9E1XdC3+8DenUgj2c7OQRkuFIDyft73zxgZuWvkrw0j3 MLa6gsPdiUwq0NMPnx/LmGInsKIJYkqV9u+1R8cwa7uEtXcCAMTlF4AhzP/lWJiqcMFw4 YRF0Ph2OauyYXKBfVn5NakWag8lVQTeWbYMHjAK6mJ9QByohAuUXH4maTNDRS1YizKQ== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none header.from=digikod.net; spf=pass smtp.mailfrom=digikod.net; dkim=pass header.d=digikod.net header.i=@digikod.net header.a=rsa-sha256 header.s=20191114 header.b=gfe3b7Pb; arc=none smtp.remote-ip=45.157.188.12 Received: from smtp-bc0c.mail.infomaniak.ch (smtp-bc0c.mail.infomaniak.ch [45.157.188.12]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (prime256v1)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A8144606A8B2 for ; Tue, 10 Nov 2026 20:30:58 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.2 smtp.subspace.kernel.org A8144606A8B2 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.a=rsa-sha256 header.s=20191114 header.b=gfe3b7Pb 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 4jPJFX6nV0zFlv; Tue, 10 Nov 2026 21:30:56 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=digikod.net; s=20191114; t=1794342656; bh=gfuupJbALZD+q1BFqL8+mKjX6fPsKBJ1ASODJI4cCsU=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=gfe3b7PbrK1pwxlTCEigGYxsIcmWXZwT8Ad9jzQ5qFilWGNYvB89XEcBukEca2Qj9 2Uba4CpIAn6cuCaLr/U8lunpukVt/AfyoXi6MvQnEUeVMBzT+Q5y3x0ClkpDIP8TPG a/TwhXuXeiMSkpWs2elLSMCS/mid6RjUP4Cl+jqs= Received: from unknown by smtp-3-0001.mail.infomaniak.ch (Postfix) with ESMTPA id 4jPJFW2pGSzxC2; Tue, 10 Nov 2026 21:30:55 +0100 (CET) From: =?UTF-8?q?Micka=C3=ABl=20Sala=C3=BCn?= To: Casey Schaufler , Georgia Garcia , =?UTF-8?q?G=C3=BCnther=20Noack?= , James Morris , John Johansen , =?UTF-8?q?Ondrej=20Mosn=C3=A1=C4=8Dek?= , Paul Moore , Serge Hallyn , Stephen Smalley Cc: =?UTF-8?q?Micka=C3=ABl=20Sala=C3=BCn?= , Christian Brauner , KP Singh , Matt Bobrowski , apparmor@lists.ubuntu.com, bpf@vger.kernel.org, kernel-team@cloudflare.com, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org, selinux@vger.kernel.org Subject: [PATCH 6/6] selftests/landlock: Test capabilities guarding other LSMs' policies Date: Tue, 10 Nov 2026 21:30:40 +0100 Message-ID: <20261110203042.2410140-7-mic@digikod.net> In-Reply-To: <20261110203042.2410140-1-mic@digikod.net> References: <20261110203042.2410140-1-mic@digikod.net> X-Mailing-List: linux-security-module@vger.kernel.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Infomaniak-Routing: alpha Check that a domain denying CAP_MAC_ADMIN prevents a process from changing a file's Smack label and from loading AppArmor policy. Also check that denying CAP_MAC_OVERRIDE prevents a process from bypassing Smack rules between sockets with distinct labels. The allowed, unhandled and unsandboxed variants retain each capability, and an audit test checks both Smack denials. Smack and AppArmor are optional and are not added to the test configuration: each test is skipped if its LSM is not active according to lsm_list_modules(2), and fails if this syscall fails. The default test configuration has no other LSM, so these tests skip there and the capable_lsm hook is not exercised. The cap_other_hooks fixture now covers more than commoncap's checks, so update its description, and add CAP_MAC_ADMIN and CAP_MAC_OVERRIDE to the capabilities that the tests may raise. Without the previous commits, the denied variants and the audit test fail with Smack (boot with lsm=landlock,smack), and the denied variants fail with AppArmor (lsm=landlock,apparmor). Test coverage for security/landlock is 91.7% of 2844 lines according to LLVM 18. Cc: Casey Schaufler Cc: Christian Brauner Cc: Günther Noack Cc: John Johansen Cc: Paul Moore Signed-off-by: Mickaël Salaün --- tools/testing/selftests/landlock/cap_test.c | 230 +++++++++++++++++++- tools/testing/selftests/landlock/common.h | 2 + 2 files changed, 227 insertions(+), 5 deletions(-) diff --git a/tools/testing/selftests/landlock/cap_test.c b/tools/testing/selftests/landlock/cap_test.c index 741567c5ab41..7f9b7fb2bade 100644 --- a/tools/testing/selftests/landlock/cap_test.c +++ b/tools/testing/selftests/landlock/cap_test.c @@ -10,12 +10,16 @@ #include #include #include +#include +#include #include #include #include #include #include #include +#include +#include #include #include #include @@ -741,11 +745,12 @@ TEST(cap_target_userns_independent) } /* - * Capability checks that commoncap makes from hooks other than the capable - * hook. Each test first checks that the operation needs the capability, then - * checks it with the capability effective. Variants cover an unsandboxed - * baseline, a domain that denies the capability, a domain that allows it, and a - * domain that does not handle capabilities. + * Capability checks that do not go through the capable hook: commoncap's checks + * from other hooks, and other LSMs' checks for their own policy. Each test + * first checks that the operation needs the capability, then checks it with the + * capability effective. Variants cover an unsandboxed baseline, a domain that + * denies the capability, a domain that allows it, and a domain that does not + * handle capabilities. */ /* clang-format off */ FIXTURE(cap_other_hooks) {}; @@ -969,6 +974,180 @@ TEST_F(cap_other_hooks, capset_inheritable) clear_cap(_metadata, CAP_SETPCAP); } +/* Much more than the number of LSMs that can be active at the same time. */ +#define LSM_IDS_MAX 64 + +static bool is_lsm_active(struct __test_metadata *const _metadata, + const __u64 lsm_id) +{ + __u64 ids[LSM_IDS_MAX]; + __u32 size = sizeof(ids); + long count, i; + + count = syscall(__NR_lsm_list_modules, ids, &size, 0); + ASSERT_LE(0, count) + { + TH_LOG("Failed to list LSMs: %s", strerror(errno)); + } + for (i = 0; i < count; i++) { + if (ids[i] == lsm_id) + return true; + } + return false; +} + +/* + * The following tests check capabilities that guard another LSM's policy, which + * that LSM checks with security_capable_lsm(). These LSMs are optional, and + * are not in the test configuration: each test is skipped if its LSM is not + * active (e.g., boot with lsm=landlock,smack). Not tested here: SELinux only + * checks CAP_MAC_ADMIN this way when a policy is loaded, and AppArmor only + * checks CAP_MAC_OVERRIDE this way for unconfined profile changes when the + * kernel.apparmor_restrict_unprivileged_unconfined sysctl is set. + */ + +/* Returns an FD to a new file on a private tmpfs. */ +static int create_private_file(struct __test_metadata *const _metadata) +{ + int fd; + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, unshare(CLONE_NEWNS)); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); + ASSERT_EQ(0, mount("test", "/tmp", "tmpfs", 0, NULL)); + clear_cap(_metadata, CAP_SYS_ADMIN); + fd = open("/tmp/f", O_CREAT | O_RDWR | O_CLOEXEC, 0600); + ASSERT_LE(0, fd); + return fd; +} + +static const char smack_out_label[] = "landlock-out"; +static const char smack_in_label[] = "landlock-in"; + +/* Creates connected sockets whose Smack labels deny writes. */ +static void create_smack_socketpair(struct __test_metadata *const _metadata, + int socket_fds[2]) +{ + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0, + socket_fds)); + set_cap(_metadata, CAP_MAC_ADMIN); + ASSERT_EQ(0, + fsetxattr(socket_fds[0], XATTR_NAME_SMACKIPOUT, + smack_out_label, sizeof(smack_out_label) - 1, 0)); + ASSERT_EQ(0, fsetxattr(socket_fds[1], XATTR_NAME_SMACKIPIN, + smack_in_label, sizeof(smack_in_label) - 1, 0)); + clear_cap(_metadata, CAP_MAC_ADMIN); +} + +/* Smack requires CAP_MAC_ADMIN to change a file's Smack label. */ +TEST_F(cap_other_hooks, smack_relabel) +{ + int fd; + + if (!is_lsm_active(_metadata, LSM_ID_SMACK)) + SKIP(return, "Smack is not active"); + + fd = create_private_file(_metadata); + enforce_cap_variant(_metadata, variant, CAP_MAC_ADMIN); + + EXPECT_EQ(-1, fsetxattr(fd, "security.SMACK64", "_", 1, 0)); + EXPECT_EQ(EPERM, errno); + + set_cap(_metadata, CAP_MAC_ADMIN); + if (variant->is_allowed) { + EXPECT_EQ(0, fsetxattr(fd, "security.SMACK64", "_", 1, 0)) + { + TH_LOG("Failed to set the Smack label: %s", + strerror(errno)); + } + } else { + EXPECT_EQ(-1, fsetxattr(fd, "security.SMACK64", "_", 1, 0)); + EXPECT_EQ(EPERM, errno); + } + clear_cap(_metadata, CAP_MAC_ADMIN); + EXPECT_EQ(0, close(fd)); +} + +/* CAP_MAC_OVERRIDE bypasses Smack label rules. */ +TEST_F(cap_other_hooks, smack_override) +{ + int socket_fds[2]; + + if (!is_lsm_active(_metadata, LSM_ID_SMACK)) + SKIP(return, "Smack is not active"); + + create_smack_socketpair(_metadata, socket_fds); + + /* The distinct Smack labels deny writes without an override. */ + EXPECT_EQ(-1, send(socket_fds[0], "x", 1, 0)); + EXPECT_EQ(EACCES, errno); + + enforce_cap_variant(_metadata, variant, CAP_MAC_OVERRIDE); + set_cap(_metadata, CAP_MAC_OVERRIDE); + if (variant->is_allowed) { + EXPECT_EQ(1, send(socket_fds[0], "x", 1, 0)); + } else { + EXPECT_EQ(-1, send(socket_fds[0], "x", 1, 0)); + EXPECT_EQ(EACCES, errno); + } + clear_cap(_metadata, CAP_MAC_OVERRIDE); + + EXPECT_EQ(0, close(socket_fds[0])); + EXPECT_EQ(0, close(socket_fds[1])); +} + +#define APPARMOR_LOAD_PATH "/sys/kernel/security/apparmor/.load" + +/* + * Returns the errno of writing to AppArmor's policy load interface, or 0 on + * success. The written data is not a valid policy: a task that may not manage + * policy gets EACCES, and any other task gets EPROTONOSUPPORT. + */ +static int write_apparmor_load(struct __test_metadata *const _metadata) +{ + int fd, err = 0; + + fd = open(APPARMOR_LOAD_PATH, O_WRONLY | O_CLOEXEC); + ASSERT_LE(0, fd) + { + TH_LOG("Failed to open %s: %s", APPARMOR_LOAD_PATH, + strerror(errno)); + } + if (write(fd, "x", 1) < 0) + err = errno; + EXPECT_EQ(0, close(fd)); + return err; +} + +/* AppArmor requires CAP_MAC_ADMIN to load policy. */ +TEST_F(cap_other_hooks, apparmor_policy_load) +{ + if (!is_lsm_active(_metadata, LSM_ID_APPARMOR)) + SKIP(return, "AppArmor is not active"); + + if (access(APPARMOR_LOAD_PATH, F_OK)) { + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, unshare(CLONE_NEWNS)); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); + ASSERT_EQ(0, mount("securityfs", "/sys/kernel/security", + "securityfs", 0, NULL)); + clear_cap(_metadata, CAP_SYS_ADMIN); + } + ASSERT_EQ(0, access(APPARMOR_LOAD_PATH, F_OK)); + + enforce_cap_variant(_metadata, variant, CAP_MAC_ADMIN); + + EXPECT_EQ(EACCES, write_apparmor_load(_metadata)); + + set_cap(_metadata, CAP_MAC_ADMIN); + if (variant->is_allowed) { + EXPECT_EQ(EPROTONOSUPPORT, write_apparmor_load(_metadata)); + } else { + EXPECT_EQ(EACCES, write_apparmor_load(_metadata)); + } + clear_cap(_metadata, CAP_MAC_ADMIN); +} + /* * With vm.overcommit_memory=2, cap_vm_enough_memory() lets CAP_SYS_ADMIN use * the vm.admin_reserve_kbytes reserve. The test sets a commit limit that @@ -1432,6 +1611,47 @@ TEST_F(cap_audit, other_hooks_allowed) EXPECT_EQ(0, records.access); } +/* Denials in the capable_lsm hook are audited like capable hook denials. */ +TEST_F(cap_audit, smack) +{ + struct audit_records records = {}; + int ruleset_fd, fd, socket_fds[2]; + __u64 domain_id; + + if (!is_lsm_active(_metadata, LSM_ID_SMACK)) + SKIP(return, "Smack is not active"); + + fd = create_private_file(_metadata); + create_smack_socketpair(_metadata, socket_fds); + ruleset_fd = create_cap_ruleset(); + ASSERT_LE(0, ruleset_fd); + /* Allow CAP_AUDIT_CONTROL for child-side audit cleanup. */ + ASSERT_EQ(0, add_cap_rule(ruleset_fd, CAP_AUDIT_CONTROL)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + set_cap(_metadata, CAP_MAC_ADMIN); + EXPECT_EQ(-1, fsetxattr(fd, "security.SMACK64", "_", 1, 0)); + EXPECT_EQ(EPERM, errno); + clear_cap(_metadata, CAP_MAC_ADMIN); + EXPECT_EQ(0, close(fd)); + + set_cap(_metadata, CAP_MAC_OVERRIDE); + EXPECT_EQ(-1, send(socket_fds[0], "x", 1, 0)); + EXPECT_EQ(EACCES, errno); + clear_cap(_metadata, CAP_MAC_OVERRIDE); + EXPECT_EQ(0, close(socket_fds[0])); + EXPECT_EQ(0, close(socket_fds[1])); + + EXPECT_EQ(0, matches_log_cap(self->audit_fd, CAP_MAC_ADMIN)); + EXPECT_EQ(0, matches_log_domain_allocated(self->audit_fd, getpid(), + &domain_id)); + EXPECT_EQ(0, matches_log_cap(self->audit_fd, CAP_MAC_OVERRIDE)); + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + TEST_F(cap_audit, allowed) { struct audit_records records = {}; diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h index a6fdfb422fad..77a8278fe89a 100644 --- a/tools/testing/selftests/landlock/common.h +++ b/tools/testing/selftests/landlock/common.h @@ -41,6 +41,8 @@ static void _init_caps(struct __test_metadata *const _metadata, bool drop_all) /* clang-format off */ CAP_AUDIT_CONTROL, CAP_DAC_OVERRIDE, + CAP_MAC_ADMIN, + CAP_MAC_OVERRIDE, CAP_MKNOD, CAP_NET_ADMIN, CAP_NET_BIND_SERVICE, -- 2.56.0