Skip to content

Commit 874c8f8

Browse files
committed
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. Cc: Günther Noack <gnoack@google.com> Cc: Jann Horn <jannh@google.com> Cc: stable@vger.kernel.org Fixes: ead9079 ("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>
1 parent 7aaa804 commit 874c8f8

2 files changed

Lines changed: 90 additions & 4 deletions

File tree

security/landlock/cred.c

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@ static void hook_cred_transfer(struct cred *const new,
2222
const struct landlock_cred_security *const old_llcred =
2323
landlock_cred(old);
2424

25-
if (old_llcred->domain) {
26-
landlock_get_ruleset(old_llcred->domain);
27-
*landlock_cred(new) = *old_llcred;
28-
}
25+
landlock_get_ruleset(old_llcred->domain);
26+
*landlock_cred(new) = *old_llcred;
2927
}
3028

3129
static int hook_cred_prepare(struct cred *const new,

tools/testing/selftests/landlock/audit_test.c

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,94 @@ TEST_F(audit, thread)
279279
&audit_tv_default, sizeof(audit_tv_default)));
280280
}
281281

282+
/*
283+
* Verifies that log_subdomains_off set via the ruleset_fd=-1 path (without
284+
* creating a domain) is inherited by children across fork(). This exercises
285+
* the hook_cred_transfer() fix: the Landlock credential blob must be copied
286+
* even when the source credential has no domain.
287+
*
288+
* Phase 1 (baseline): a child without muting creates a domain and triggers a
289+
* denial that IS logged.
290+
*
291+
* Phase 2 (after muting): the parent mutes subdomain logs, forks another child
292+
* who creates a domain and triggers a denial that is NOT logged.
293+
*/
294+
TEST_F(audit, log_subdomains_off_fork)
295+
{
296+
const struct landlock_ruleset_attr ruleset_attr = {
297+
.scoped = LANDLOCK_SCOPE_SIGNAL,
298+
};
299+
struct audit_records records;
300+
int ruleset_fd, status;
301+
pid_t child;
302+
303+
ruleset_fd =
304+
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
305+
ASSERT_LE(0, ruleset_fd);
306+
307+
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
308+
309+
/*
310+
* Phase 1: forks a child that creates a domain and triggers a denial
311+
* before any muting. This proves the audit path works.
312+
*/
313+
child = fork();
314+
ASSERT_LE(0, child);
315+
if (child == 0) {
316+
ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
317+
ASSERT_EQ(-1, kill(getppid(), 0));
318+
ASSERT_EQ(EPERM, errno);
319+
_exit(0);
320+
return;
321+
}
322+
323+
ASSERT_EQ(child, waitpid(child, &status, 0));
324+
ASSERT_EQ(true, WIFEXITED(status));
325+
ASSERT_EQ(0, WEXITSTATUS(status));
326+
327+
/* The denial must be logged (baseline). */
328+
EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, getpid(),
329+
NULL));
330+
331+
/* Drains any remaining records (e.g. domain allocation). */
332+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
333+
334+
/*
335+
* Mutes subdomain logs without creating a domain. The parent's
336+
* credential has domain=NULL and log_subdomains_off=1.
337+
*/
338+
ASSERT_EQ(0, landlock_restrict_self(
339+
-1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
340+
341+
/*
342+
* Phase 2: forks a child that creates a domain and triggers a denial.
343+
* Because log_subdomains_off was inherited via fork(), the child's
344+
* domain has log_status=LANDLOCK_LOG_DISABLED.
345+
*/
346+
child = fork();
347+
ASSERT_LE(0, child);
348+
if (child == 0) {
349+
ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
350+
ASSERT_EQ(-1, kill(getppid(), 0));
351+
ASSERT_EQ(EPERM, errno);
352+
_exit(0);
353+
return;
354+
}
355+
356+
ASSERT_EQ(child, waitpid(child, &status, 0));
357+
ASSERT_EQ(true, WIFEXITED(status));
358+
ASSERT_EQ(0, WEXITSTATUS(status));
359+
360+
/* No denial record should appear. */
361+
EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
362+
getpid(), NULL));
363+
364+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
365+
EXPECT_EQ(0, records.access);
366+
367+
EXPECT_EQ(0, close(ruleset_fd));
368+
}
369+
282370
FIXTURE(audit_flags)
283371
{
284372
struct audit_filter audit_filter;

0 commit comments

Comments
 (0)