Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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: ead9079f7569 ("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>

+90 -4
+2 -4
security/landlock/cred.c
··· 22 22 const struct landlock_cred_security *const old_llcred = 23 23 landlock_cred(old); 24 24 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; 29 27 } 30 28 31 29 static int hook_cred_prepare(struct cred *const new,
+88
tools/testing/selftests/landlock/audit_test.c
··· 279 279 &audit_tv_default, sizeof(audit_tv_default))); 280 280 } 281 281 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 + 282 370 FIXTURE(audit_flags) 283 371 { 284 372 struct audit_filter audit_filter;