landlock: Fix LOG_SUBDOMAINS_OFF inheritance across fork()

commit 874c8f8382 upstream.

hook_cred_transfer() only copies the Landlock security blob when the
source credential has a domain.  This is inconsistent with
landlock_restrict_self() which can set LOG_SUBDOMAINS_OFF on a
credential without creating a domain (via the ruleset_fd=-1 path): the
field is committed but not preserved across fork() because the child's
prepare_creds() calls hook_cred_transfer() which skips the copy when
domain is NULL.

This breaks the documented use case where a process mutes subdomain logs
before forking sandboxed children: the children lose the muting and
their domains produce unexpected audit records.

Fix this by unconditionally copying the Landlock credential blob.

Cc: Günther Noack <gnoack@google.com>
Cc: Jann Horn <jannh@google.com>
Cc: stable@vger.kernel.org
Fixes: ead9079f75 ("landlock: Add LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF")
Reviewed-by: Günther Noack <gnoack3000@gmail.com>
Link: https://lore.kernel.org/r/20260407164107.2012589-1-mic@digikod.net
Signed-off-by: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
Mickaël Salaün
2026-05-07 06:11:41 +02:00
committed by Greg Kroah-Hartman
parent df865a3bb9
commit 2fcde49092
2 changed files with 90 additions and 4 deletions
+2 -4
View File
@@ -22,10 +22,8 @@ static void hook_cred_transfer(struct cred *const new,
const struct landlock_cred_security *const old_llcred =
landlock_cred(old);
if (old_llcred->domain) {
landlock_get_ruleset(old_llcred->domain);
*landlock_cred(new) = *old_llcred;
}
landlock_get_ruleset(old_llcred->domain);
*landlock_cred(new) = *old_llcred;
}
static int hook_cred_prepare(struct cred *const new,
@@ -279,6 +279,94 @@ TEST_F(audit, thread)
&audit_tv_default, sizeof(audit_tv_default)));
}
/*
* Verifies that log_subdomains_off set via the ruleset_fd=-1 path (without
* creating a domain) is inherited by children across fork(). This exercises
* the hook_cred_transfer() fix: the Landlock credential blob must be copied
* even when the source credential has no domain.
*
* Phase 1 (baseline): a child without muting creates a domain and triggers a
* denial that IS logged.
*
* Phase 2 (after muting): the parent mutes subdomain logs, forks another child
* who creates a domain and triggers a denial that is NOT logged.
*/
TEST_F(audit, log_subdomains_off_fork)
{
const struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
struct audit_records records;
int ruleset_fd, status;
pid_t child;
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
/*
* Phase 1: forks a child that creates a domain and triggers a denial
* before any muting. This proves the audit path works.
*/
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
ASSERT_EQ(-1, kill(getppid(), 0));
ASSERT_EQ(EPERM, errno);
_exit(0);
return;
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_EQ(true, WIFEXITED(status));
ASSERT_EQ(0, WEXITSTATUS(status));
/* The denial must be logged (baseline). */
EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, getpid(),
NULL));
/* Drains any remaining records (e.g. domain allocation). */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
/*
* Mutes subdomain logs without creating a domain. The parent's
* credential has domain=NULL and log_subdomains_off=1.
*/
ASSERT_EQ(0, landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
/*
* Phase 2: forks a child that creates a domain and triggers a denial.
* Because log_subdomains_off was inherited via fork(), the child's
* domain has log_status=LANDLOCK_LOG_DISABLED.
*/
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
ASSERT_EQ(-1, kill(getppid(), 0));
ASSERT_EQ(EPERM, errno);
_exit(0);
return;
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_EQ(true, WIFEXITED(status));
ASSERT_EQ(0, WEXITSTATUS(status));
/* No denial record should appear. */
EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
getpid(), NULL));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, close(ruleset_fd));
}
FIXTURE(audit_flags)
{
struct audit_filter audit_filter;