Skip to content

Commit e75e380

Browse files
committed
landlock: Allow TSYNC with LOG_SUBDOMAINS_OFF and fd=-1
LANDLOCK_RESTRICT_SELF_TSYNC does not allow LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with ruleset_fd=-1, preventing a multithreaded process from atomically propagating subdomain log muting to all threads without creating a domain layer. Relax the fd=-1 condition to accept TSYNC alongside LOG_SUBDOMAINS_OFF, and update the documentation accordingly. Add flag validation tests for all TSYNC combinations with ruleset_fd=-1, and audit tests verifying both transition directions: muting via TSYNC (logged to not logged) and override via TSYNC (not logged to logged). Cc: Günther Noack <gnoack@google.com> Cc: stable@vger.kernel.org Fixes: 42fc7e6 ("landlock: Multithreading support for landlock_restrict_self()") Reviewed-by: Günther Noack <gnoack3000@gmail.com> Link: https://lore.kernel.org/r/20260407164107.2012589-2-mic@digikod.net Signed-off-by: Mickaël Salaün <mic@digikod.net>
1 parent 874c8f8 commit e75e380

4 files changed

Lines changed: 322 additions & 6 deletions

File tree

include/uapi/linux/landlock.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ struct landlock_ruleset_attr {
116116
* ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``, this flag only affects
117117
* future nested domains, not the one being created. It can also be used
118118
* with a @ruleset_fd value of -1 to mute subdomain logs without creating a
119-
* domain.
119+
* domain. When combined with %LANDLOCK_RESTRICT_SELF_TSYNC and a
120+
* @ruleset_fd value of -1, this configuration is propagated to all threads
121+
* of the current process.
120122
*
121123
* The following flag supports policy enforcement in multithreaded processes:
122124
*

security/landlock/syscalls.c

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -512,10 +512,13 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
512512

513513
/*
514514
* It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
515-
* -1 as ruleset_fd, but no other flag must be set.
515+
* -1 as ruleset_fd, optionally combined with
516+
* LANDLOCK_RESTRICT_SELF_TSYNC to propagate this configuration to all
517+
* threads. No other flag must be set.
516518
*/
517519
if (!(ruleset_fd == -1 &&
518-
flags == LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
520+
(flags & ~LANDLOCK_RESTRICT_SELF_TSYNC) ==
521+
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
519522
/* Gets and checks the ruleset. */
520523
ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ);
521524
if (IS_ERR(ruleset))
@@ -537,9 +540,10 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
537540

538541
/*
539542
* The only case when a ruleset may not be set is if
540-
* LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set and ruleset_fd is -1.
541-
* We could optimize this case by not calling commit_creds() if this flag
542-
* was already set, but it is not worth the complexity.
543+
* LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set (optionally with
544+
* LANDLOCK_RESTRICT_SELF_TSYNC) and ruleset_fd is -1. We could
545+
* optimize this case by not calling commit_creds() if this flag was
546+
* already set, but it is not worth the complexity.
543547
*/
544548
if (ruleset) {
545549
/*

tools/testing/selftests/landlock/audit_test.c

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ TEST_F(audit, layers)
162162
struct thread_data {
163163
pid_t parent_pid;
164164
int ruleset_fd, pipe_child, pipe_parent;
165+
bool mute_subdomains;
165166
};
166167

167168
static void *thread_audit_test(void *arg)
@@ -367,6 +368,238 @@ TEST_F(audit, log_subdomains_off_fork)
367368
EXPECT_EQ(0, close(ruleset_fd));
368369
}
369370

371+
/*
372+
* Thread function: runs two rounds of (create domain, trigger denial, signal
373+
* back), waiting for the main thread before each round. When mute_subdomains
374+
* is set, phase 1 also mutes subdomain logs via the fd=-1 path before creating
375+
* the domain. The ruleset_fd is kept open across both rounds so each
376+
* restrict_self call stacks a new domain layer.
377+
*/
378+
static void *thread_sandbox_deny_twice(void *arg)
379+
{
380+
const struct thread_data *data = (struct thread_data *)arg;
381+
uintptr_t err = 0;
382+
char buffer;
383+
384+
/* Phase 1: optionally mutes, creates a domain, and triggers a denial. */
385+
if (read(data->pipe_parent, &buffer, 1) != 1) {
386+
err = 1;
387+
goto out;
388+
}
389+
390+
if (data->mute_subdomains &&
391+
landlock_restrict_self(-1,
392+
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
393+
err = 2;
394+
goto out;
395+
}
396+
397+
if (landlock_restrict_self(data->ruleset_fd, 0)) {
398+
err = 3;
399+
goto out;
400+
}
401+
402+
if (kill(data->parent_pid, 0) != -1 || errno != EPERM) {
403+
err = 4;
404+
goto out;
405+
}
406+
407+
if (write(data->pipe_child, ".", 1) != 1) {
408+
err = 5;
409+
goto out;
410+
}
411+
412+
/* Phase 2: stacks another domain and triggers a denial. */
413+
if (read(data->pipe_parent, &buffer, 1) != 1) {
414+
err = 6;
415+
goto out;
416+
}
417+
418+
if (landlock_restrict_self(data->ruleset_fd, 0)) {
419+
err = 7;
420+
goto out;
421+
}
422+
423+
if (kill(data->parent_pid, 0) != -1 || errno != EPERM) {
424+
err = 8;
425+
goto out;
426+
}
427+
428+
if (write(data->pipe_child, ".", 1) != 1) {
429+
err = 9;
430+
goto out;
431+
}
432+
433+
out:
434+
close(data->ruleset_fd);
435+
close(data->pipe_child);
436+
close(data->pipe_parent);
437+
return (void *)err;
438+
}
439+
440+
/*
441+
* Verifies that LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
442+
* LANDLOCK_RESTRICT_SELF_TSYNC and ruleset_fd=-1 propagates log_subdomains_off
443+
* to a sibling thread, suppressing audit logging on domains it subsequently
444+
* creates.
445+
*
446+
* Phase 1 (before TSYNC) acts as an inline baseline: the sibling creates a
447+
* domain and triggers a denial that IS logged.
448+
*
449+
* Phase 2 (after TSYNC) verifies suppression: the sibling stacks another domain
450+
* and triggers a denial that is NOT logged.
451+
*/
452+
TEST_F(audit, log_subdomains_off_tsync)
453+
{
454+
const struct landlock_ruleset_attr ruleset_attr = {
455+
.scoped = LANDLOCK_SCOPE_SIGNAL,
456+
};
457+
struct audit_records records;
458+
struct thread_data child_data = {};
459+
int pipe_child[2], pipe_parent[2];
460+
char buffer;
461+
pthread_t thread;
462+
void *thread_ret;
463+
464+
child_data.parent_pid = getppid();
465+
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
466+
child_data.pipe_child = pipe_child[1];
467+
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
468+
child_data.pipe_parent = pipe_parent[0];
469+
child_data.ruleset_fd =
470+
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
471+
ASSERT_LE(0, child_data.ruleset_fd);
472+
473+
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
474+
475+
/* Creates the sibling thread. */
476+
ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice,
477+
&child_data));
478+
479+
/*
480+
* Phase 1: the sibling creates a domain and triggers a denial before
481+
* any log muting. This proves the audit path works.
482+
*/
483+
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
484+
ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
485+
486+
/* The denial must be logged. */
487+
EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
488+
child_data.parent_pid, NULL));
489+
490+
/* Drains any remaining records (e.g. domain allocation). */
491+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
492+
493+
/*
494+
* Mutes subdomain logs and propagates to the sibling thread via TSYNC,
495+
* without creating a domain.
496+
*/
497+
ASSERT_EQ(0, landlock_restrict_self(
498+
-1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
499+
LANDLOCK_RESTRICT_SELF_TSYNC));
500+
501+
/*
502+
* Phase 2: the sibling stacks another domain and triggers a denial.
503+
* Because log_subdomains_off was propagated via TSYNC, the new domain
504+
* has log_status=LANDLOCK_LOG_DISABLED.
505+
*/
506+
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
507+
ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
508+
509+
/* No denial record should appear. */
510+
EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
511+
child_data.parent_pid, NULL));
512+
513+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
514+
EXPECT_EQ(0, records.access);
515+
516+
EXPECT_EQ(0, close(pipe_child[0]));
517+
EXPECT_EQ(0, close(pipe_parent[1]));
518+
ASSERT_EQ(0, pthread_join(thread, &thread_ret));
519+
EXPECT_EQ(NULL, thread_ret);
520+
}
521+
522+
/*
523+
* Verifies that LANDLOCK_RESTRICT_SELF_TSYNC without
524+
* LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF overrides a sibling thread's
525+
* log_subdomains_off, re-enabling audit logging on domains the sibling
526+
* subsequently creates.
527+
*
528+
* Phase 1: the sibling sets log_subdomains_off, creates a muted domain, and
529+
* triggers a denial that is NOT logged.
530+
*
531+
* Phase 2 (after TSYNC without LOG_SUBDOMAINS_OFF): the sibling stacks another
532+
* domain and triggers a denial that IS logged, proving the muting was
533+
* overridden.
534+
*/
535+
TEST_F(audit, tsync_override_log_subdomains_off)
536+
{
537+
const struct landlock_ruleset_attr ruleset_attr = {
538+
.scoped = LANDLOCK_SCOPE_SIGNAL,
539+
};
540+
struct audit_records records;
541+
struct thread_data child_data = {};
542+
int pipe_child[2], pipe_parent[2];
543+
char buffer;
544+
pthread_t thread;
545+
void *thread_ret;
546+
547+
child_data.parent_pid = getppid();
548+
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
549+
child_data.pipe_child = pipe_child[1];
550+
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
551+
child_data.pipe_parent = pipe_parent[0];
552+
child_data.ruleset_fd =
553+
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
554+
ASSERT_LE(0, child_data.ruleset_fd);
555+
556+
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
557+
558+
child_data.mute_subdomains = true;
559+
560+
/* Creates the sibling thread. */
561+
ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice,
562+
&child_data));
563+
564+
/*
565+
* Phase 1: the sibling mutes subdomain logs, creates a domain, and
566+
* triggers a denial. The denial must not be logged.
567+
*/
568+
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
569+
ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
570+
571+
EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
572+
child_data.parent_pid, NULL));
573+
574+
/* Drains any remaining records. */
575+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
576+
EXPECT_EQ(0, records.access);
577+
578+
/*
579+
* Overrides the sibling's log_subdomains_off by calling TSYNC without
580+
* LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF.
581+
*/
582+
ASSERT_EQ(0, landlock_restrict_self(child_data.ruleset_fd,
583+
LANDLOCK_RESTRICT_SELF_TSYNC));
584+
585+
/*
586+
* Phase 2: the sibling stacks another domain and triggers a denial.
587+
* Because TSYNC replaced its log_subdomains_off with 0, the new domain
588+
* has log_status=LANDLOCK_LOG_PENDING.
589+
*/
590+
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
591+
ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
592+
593+
/* The denial must be logged. */
594+
EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
595+
child_data.parent_pid, NULL));
596+
597+
EXPECT_EQ(0, close(pipe_child[0]));
598+
EXPECT_EQ(0, close(pipe_parent[1]));
599+
ASSERT_EQ(0, pthread_join(thread, &thread_ret));
600+
EXPECT_EQ(NULL, thread_ret);
601+
}
602+
370603
FIXTURE(audit_flags)
371604
{
372605
struct audit_filter audit_filter;

tools/testing/selftests/landlock/tsync_test.c

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,81 @@ TEST(tsync_interrupt)
247247
EXPECT_EQ(0, close(ruleset_fd));
248248
}
249249

250+
/* clang-format off */
251+
FIXTURE(tsync_without_ruleset) {};
252+
/* clang-format on */
253+
254+
FIXTURE_VARIANT(tsync_without_ruleset)
255+
{
256+
const __u32 flags;
257+
const int expected_errno;
258+
};
259+
260+
/* clang-format off */
261+
FIXTURE_VARIANT_ADD(tsync_without_ruleset, tsync_only) {
262+
/* clang-format on */
263+
.flags = LANDLOCK_RESTRICT_SELF_TSYNC,
264+
.expected_errno = EBADF,
265+
};
266+
267+
/* clang-format off */
268+
FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_same_exec_off) {
269+
/* clang-format on */
270+
.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
271+
LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
272+
LANDLOCK_RESTRICT_SELF_TSYNC,
273+
.expected_errno = EBADF,
274+
};
275+
276+
/* clang-format off */
277+
FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_new_exec_on) {
278+
/* clang-format on */
279+
.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
280+
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
281+
LANDLOCK_RESTRICT_SELF_TSYNC,
282+
.expected_errno = EBADF,
283+
};
284+
285+
/* clang-format off */
286+
FIXTURE_VARIANT_ADD(tsync_without_ruleset, all_flags) {
287+
/* clang-format on */
288+
.flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
289+
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
290+
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
291+
LANDLOCK_RESTRICT_SELF_TSYNC,
292+
.expected_errno = EBADF,
293+
};
294+
295+
/* clang-format off */
296+
FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off) {
297+
/* clang-format on */
298+
.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
299+
LANDLOCK_RESTRICT_SELF_TSYNC,
300+
.expected_errno = 0,
301+
};
302+
303+
FIXTURE_SETUP(tsync_without_ruleset)
304+
{
305+
disable_caps(_metadata);
306+
}
307+
308+
FIXTURE_TEARDOWN(tsync_without_ruleset)
309+
{
310+
}
311+
312+
TEST_F(tsync_without_ruleset, check)
313+
{
314+
int ret;
315+
316+
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
317+
318+
ret = landlock_restrict_self(-1, variant->flags);
319+
if (variant->expected_errno) {
320+
EXPECT_EQ(-1, ret);
321+
EXPECT_EQ(variant->expected_errno, errno);
322+
} else {
323+
EXPECT_EQ(0, ret);
324+
}
325+
}
326+
250327
TEST_HARNESS_MAIN

0 commit comments

Comments
 (0)