Skip to content

Commit e2893c0

Browse files
committed
selftests/landlock: Add audit tests for ptrace
Add tests for all ptrace actions checking "blockers=ptrace" records. This also improves PTRACE_TRACEME and PTRACE_ATTACH tests by making sure that the restrictions comes from Landlock, and with the expected process. These extended tests are like enhanced errno checks that make sure Landlock enforcement is consistent. Cc: Günther Noack <gnoack@google.com> Cc: Paul Moore <paul@paul-moore.com> Link: https://lore.kernel.org/r/20250320190717.2287696-25-mic@digikod.net Signed-off-by: Mickaël Salaün <mic@digikod.net>
1 parent 960ed6c commit e2893c0

File tree

1 file changed

+140
-0
lines changed

1 file changed

+140
-0
lines changed

tools/testing/selftests/landlock/ptrace_test.c

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*
55
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
66
* Copyright © 2019-2020 ANSSI
7+
* Copyright © 2024-2025 Microsoft Corporation
78
*/
89

910
#define _GNU_SOURCE
@@ -17,6 +18,7 @@
1718
#include <sys/wait.h>
1819
#include <unistd.h>
1920

21+
#include "audit.h"
2022
#include "common.h"
2123

2224
/* Copied from security/yama/yama_lsm.c */
@@ -434,4 +436,142 @@ TEST_F(hierarchy, trace)
434436
_metadata->exit_code = KSFT_FAIL;
435437
}
436438

439+
static int matches_log_ptrace(struct __test_metadata *const _metadata,
440+
int audit_fd, const pid_t opid)
441+
{
442+
static const char log_template[] = REGEX_LANDLOCK_PREFIX
443+
" blockers=ptrace opid=%d ocomm=\"ptrace_test\"$";
444+
char log_match[sizeof(log_template) + 10];
445+
int log_match_len;
446+
447+
log_match_len =
448+
snprintf(log_match, sizeof(log_match), log_template, opid);
449+
if (log_match_len > sizeof(log_match))
450+
return -E2BIG;
451+
452+
return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match,
453+
NULL);
454+
}
455+
456+
FIXTURE(audit)
457+
{
458+
struct audit_filter audit_filter;
459+
int audit_fd;
460+
};
461+
462+
FIXTURE_SETUP(audit)
463+
{
464+
disable_caps(_metadata);
465+
set_cap(_metadata, CAP_AUDIT_CONTROL);
466+
self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
467+
EXPECT_LE(0, self->audit_fd);
468+
clear_cap(_metadata, CAP_AUDIT_CONTROL);
469+
}
470+
471+
FIXTURE_TEARDOWN_PARENT(audit)
472+
{
473+
EXPECT_EQ(0, audit_cleanup(-1, NULL));
474+
}
475+
476+
/* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */
477+
TEST_F(audit, trace)
478+
{
479+
pid_t child;
480+
int status;
481+
int pipe_child[2], pipe_parent[2];
482+
int yama_ptrace_scope;
483+
char buf_parent;
484+
struct audit_records records;
485+
486+
/* Makes sure there is no superfluous logged records. */
487+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
488+
EXPECT_EQ(0, records.access);
489+
EXPECT_EQ(0, records.domain);
490+
491+
yama_ptrace_scope = get_yama_ptrace_scope();
492+
ASSERT_LE(0, yama_ptrace_scope);
493+
494+
if (yama_ptrace_scope > YAMA_SCOPE_DISABLED)
495+
TH_LOG("Incomplete tests due to Yama restrictions (scope %d)",
496+
yama_ptrace_scope);
497+
498+
/*
499+
* Removes all effective and permitted capabilities to not interfere
500+
* with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS.
501+
*/
502+
drop_caps(_metadata);
503+
504+
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
505+
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
506+
507+
child = fork();
508+
ASSERT_LE(0, child);
509+
if (child == 0) {
510+
char buf_child;
511+
512+
ASSERT_EQ(0, close(pipe_parent[1]));
513+
ASSERT_EQ(0, close(pipe_child[0]));
514+
515+
/* Waits for the parent to be in a domain, if any. */
516+
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
517+
518+
/* Tests child PTRACE_TRACEME. */
519+
EXPECT_EQ(-1, ptrace(PTRACE_TRACEME));
520+
EXPECT_EQ(EPERM, errno);
521+
/* We should see the child process. */
522+
EXPECT_EQ(0, matches_log_ptrace(_metadata, self->audit_fd,
523+
getpid()));
524+
525+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
526+
EXPECT_EQ(0, records.access);
527+
/* Checks for a domain creation. */
528+
EXPECT_EQ(1, records.domain);
529+
530+
/*
531+
* Signals that the PTRACE_ATTACH test is done and the
532+
* PTRACE_TRACEME test is ongoing.
533+
*/
534+
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
535+
536+
/* Waits for the parent PTRACE_ATTACH test. */
537+
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
538+
_exit(_metadata->exit_code);
539+
return;
540+
}
541+
542+
ASSERT_EQ(0, close(pipe_child[1]));
543+
ASSERT_EQ(0, close(pipe_parent[0]));
544+
create_domain(_metadata);
545+
546+
/* Signals that the parent is in a domain. */
547+
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
548+
549+
/*
550+
* Waits for the child to test PTRACE_ATTACH on the parent and start
551+
* testing PTRACE_TRACEME.
552+
*/
553+
ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
554+
555+
/* The child should not be traced by the parent. */
556+
EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0));
557+
EXPECT_EQ(ESRCH, errno);
558+
559+
/* Tests PTRACE_ATTACH on the child. */
560+
EXPECT_EQ(-1, ptrace(PTRACE_ATTACH, child, NULL, 0));
561+
EXPECT_EQ(EPERM, errno);
562+
EXPECT_EQ(0, matches_log_ptrace(_metadata, self->audit_fd, child));
563+
564+
/* Signals that the parent PTRACE_ATTACH test is done. */
565+
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
566+
ASSERT_EQ(child, waitpid(child, &status, 0));
567+
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
568+
WEXITSTATUS(status) != EXIT_SUCCESS)
569+
_metadata->exit_code = KSFT_FAIL;
570+
571+
/* Makes sure there is no superfluous logged records. */
572+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
573+
EXPECT_EQ(0, records.access);
574+
EXPECT_EQ(0, records.domain);
575+
}
576+
437577
TEST_HARNESS_MAIN

0 commit comments

Comments
 (0)