VYPR
Unrated severityNVD Advisory· Published May 27, 2026· Updated May 27, 2026

CVE-2026-46057

CVE-2026-46057

Description

In the Linux kernel, the following vulnerability has been resolved:

landlock: Fix LOG_SUBDOMAINS_OFF inheritance across fork()

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.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Landlock flag LOG_SUBDOMAINS_OFF is not inherited across fork() due to a missing credential blob copy, causing unintended audit logs.

Vulnerability

In the Linux kernel Landlock security module, hook_cred_transfer() omits copying the credential security blob when the source credential has no Landlock domain. Since landlock_restrict_self() can set the LOG_SUBDOMAINS_OFF flag on a credential without creating a domain (via ruleset_fd=-1), this flag is lost on fork because the child’s prepare_creds() triggers hook_cred_transfer() which skips the copy when the source domain is NULL [1].

Exploitation

No special privilege or authentication is required beyond the ability to call landlock_restrict_self() with ruleset_fd=-1 and then fork. An unprivileged process that sets LOG_SUBDOMAINS_OFF to mute subdomain audit logs and then calls fork() will have the child credential lose the flag. The child process subsequently creating a Landlock domain will generate unexpected audit log entries for subdomain operations.

Impact

A process that intended to suppress subdomain audit records (e.g., to reduce noise or avoid logging sensitive actions) cannot reliably propagate that setting to child processes. This can result in unintended information disclosure via audit logs, as subdomain events from the child domain are recorded despite the parent’s explicit mute. The confidentiality and operational expectations of users relying on this feature are undermined.

Mitigation

The fix is applied upstream in commit 1c513b8a00df, which unconditionally copies the Landlock credential blob in hook_cred_transfer(). Affected versions include all Linux kernel releases containing the Landlock LOG_SUBDOMAINS_OFF feature prior to this patch. Users should update to a kernel including this commit or the relevant stable release. No workaround is available [1].

AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

6
874c8f83826c

landlock: Fix LOG_SUBDOMAINS_OFF inheritance across fork()

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitMickaël SalaünFixed in 7.1-rc1via kernel-cna
4 files changed · +180 10
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    
2fcde49092aa

landlock: Fix LOG_SUBDOMAINS_OFF inheritance across fork()

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitMickaël SalaünFixed in 6.18.27via kernel-cna
4 files changed · +180 10
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    
1c513b8a00df

landlock: Fix LOG_SUBDOMAINS_OFF inheritance across fork()

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitMickaël SalaünFixed in 7.0.4via kernel-cna
4 files changed · +180 10
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    
2fcde49092aa

landlock: Fix LOG_SUBDOMAINS_OFF inheritance across fork()

4 files changed · +180 10
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    
874c8f83826c

landlock: Fix LOG_SUBDOMAINS_OFF inheritance across fork()

4 files changed · +180 10
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    
1c513b8a00df

landlock: Fix LOG_SUBDOMAINS_OFF inheritance across fork()

4 files changed · +180 10
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • security/landlock/cred.c+2 4 modified
    diff --git a/security/landlock/cred.c b/security/landlock/cred.c
    index 0cb3edde4d18ab..cc419de75cd6bd 100644
    --- a/security/landlock/cred.c
    +++ b/security/landlock/cred.c
    @@ -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,
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    
  • tools/testing/selftests/landlock/audit_test.c+88 1 modified
    diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
    index 46d02d49835aae..20099b8667e754 100644
    --- a/tools/testing/selftests/landlock/audit_test.c
    +++ b/tools/testing/selftests/landlock/audit_test.c
    @@ -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;
    -- 
    cgit 1.3-korg
    
    
    

Vulnerability mechanics

Root cause

"Conditional copy in hook_cred_transfer() skips the Landlock credential blob when domain is NULL, losing the LOG_SUBDOMAINS_OFF flag across fork()."

Attack vector

A process calls landlock_restrict_self() with ruleset_fd=-1 and the LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF flag, which sets log_subdomains_off on the credential without creating a domain (domain remains NULL). When that process forks, prepare_creds() invokes hook_cred_transfer() in security/landlock/cred.c, which previously only copied the Landlock security blob if old_llcred->domain was non-NULL. Because domain is NULL, the copy is skipped and the child's credential loses the log_subdomains_off flag. The child then creates a domain via landlock_restrict_self() with a valid ruleset_fd, and its domain produces audit records for denials that the parent intended to suppress.

Affected code

The vulnerable function is hook_cred_transfer() in security/landlock/cred.c. The old code wrapped the blob copy in if (old_llcred->domain) { ... }, causing the entire Landlock credential security blob to be dropped during fork() when the source credential had no domain.

What the fix does

The patch in security/landlock/cred.c removes the conditional guard around the blob copy in hook_cred_transfer(). Previously the function only copied the Landlock credential (and called landlock_get_ruleset()) when old_llcred->domain was non-NULL. Now it unconditionally copies the entire blob via *landlock_cred(new) = *old_llcred and always calls landlock_get_ruleset(old_llcred->domain) — which safely handles a NULL domain because landlock_get_ruleset() is a no-op on NULL. This ensures that the log_subdomains_off flag (and any future non-domain fields in the blob) are preserved across fork() even when no Landlock domain has been created.

Preconditions

  • inputThe attacker must be able to call landlock_restrict_self() with ruleset_fd=-1 and LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF to set the log_subdomains_off flag without creating a domain.
  • inputThe process must then fork() before creating a Landlock domain in the child.

Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.