root/tools/testing/selftests/landlock/ptrace_test.c
// SPDX-License-Identifier: GPL-2.0
/*
 * Landlock tests - Ptrace
 *
 * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
 * Copyright © 2019-2020 ANSSI
 * Copyright © 2024-2025 Microsoft Corporation
 */

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <signal.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include "audit.h"
#include "common.h"

/* Copied from security/yama/yama_lsm.c */
#define YAMA_SCOPE_DISABLED 0
#define YAMA_SCOPE_RELATIONAL 1

static void create_domain(struct __test_metadata *const _metadata)
{
        int ruleset_fd;
        struct landlock_ruleset_attr ruleset_attr = {
                .handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_BLOCK,
        };

        ruleset_fd =
                landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
        EXPECT_LE(0, ruleset_fd)
        {
                TH_LOG("Failed to create a ruleset: %s", strerror(errno));
        }
        EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
        EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
        EXPECT_EQ(0, close(ruleset_fd));
}

static int test_ptrace_read(const pid_t pid)
{
        static const char path_template[] = "/proc/%d/environ";
        char procenv_path[sizeof(path_template) + 10];
        int procenv_path_size, fd;

        procenv_path_size = snprintf(procenv_path, sizeof(procenv_path),
                                     path_template, pid);
        if (procenv_path_size >= sizeof(procenv_path))
                return E2BIG;

        fd = open(procenv_path, O_RDONLY | O_CLOEXEC);
        if (fd < 0)
                return errno;
        /*
         * Mixing error codes from close(2) and open(2) should not lead to any
         * (access type) confusion for this test.
         */
        if (close(fd) != 0)
                return errno;
        return 0;
}

static int get_yama_ptrace_scope(void)
{
        int ret;
        char buf[2] = {};
        const int fd = open("/proc/sys/kernel/yama/ptrace_scope", O_RDONLY);

        if (fd < 0)
                return 0;

        if (read(fd, buf, 1) < 0) {
                close(fd);
                return -1;
        }

        ret = atoi(buf);
        close(fd);
        return ret;
}

/* clang-format off */
FIXTURE(scoped_domains) {};
/* clang-format on */

/*
 * Test multiple tracing combinations between a parent process P1 and a child
 * process P2.
 *
 * Yama's scoped ptrace is presumed disabled.  If enabled, this optional
 * restriction is enforced in addition to any Landlock check, which means that
 * all P2 requests to trace P1 would be denied.
 */
#include "scoped_base_variants.h"

FIXTURE_SETUP(scoped_domains)
{
}

FIXTURE_TEARDOWN(scoped_domains)
{
}

/* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */
TEST_F(scoped_domains, trace)
{
        pid_t child, parent;
        int status, err_proc_read;
        int pipe_child[2], pipe_parent[2];
        int yama_ptrace_scope;
        char buf_parent;
        long ret;
        bool can_read_child, can_trace_child, can_read_parent, can_trace_parent;

        yama_ptrace_scope = get_yama_ptrace_scope();
        ASSERT_LE(0, yama_ptrace_scope);

        if (yama_ptrace_scope > YAMA_SCOPE_DISABLED)
                TH_LOG("Incomplete tests due to Yama restrictions (scope %d)",
                       yama_ptrace_scope);

        /*
         * can_read_child is true if a parent process can read its child
         * process, which is only the case when the parent process is not
         * isolated from the child with a dedicated Landlock domain.
         */
        can_read_child = !variant->domain_parent;

        /*
         * can_trace_child is true if a parent process can trace its child
         * process.  This depends on two conditions:
         * - The parent process is not isolated from the child with a dedicated
         *   Landlock domain.
         * - Yama allows tracing children (up to YAMA_SCOPE_RELATIONAL).
         */
        can_trace_child = can_read_child &&
                          yama_ptrace_scope <= YAMA_SCOPE_RELATIONAL;

        /*
         * can_read_parent is true if a child process can read its parent
         * process, which is only the case when the child process is not
         * isolated from the parent with a dedicated Landlock domain.
         */
        can_read_parent = !variant->domain_child;

        /*
         * can_trace_parent is true if a child process can trace its parent
         * process.  This depends on two conditions:
         * - The child process is not isolated from the parent with a dedicated
         *   Landlock domain.
         * - Yama is disabled (YAMA_SCOPE_DISABLED).
         */
        can_trace_parent = can_read_parent &&
                           yama_ptrace_scope <= YAMA_SCOPE_DISABLED;

        /*
         * Removes all effective and permitted capabilities to not interfere
         * with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS.
         */
        drop_caps(_metadata);

        parent = getpid();
        ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
        ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
        if (variant->domain_both) {
                create_domain(_metadata);
                if (!__test_passed(_metadata))
                        /* Aborts before forking. */
                        return;
        }

        child = fork();
        ASSERT_LE(0, child);
        if (child == 0) {
                char buf_child;

                ASSERT_EQ(0, close(pipe_parent[1]));
                ASSERT_EQ(0, close(pipe_child[0]));
                if (variant->domain_child)
                        create_domain(_metadata);

                /* Waits for the parent to be in a domain, if any. */
                ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));

                /* Tests PTRACE_MODE_READ on the parent. */
                err_proc_read = test_ptrace_read(parent);
                if (can_read_parent) {
                        EXPECT_EQ(0, err_proc_read);
                } else {
                        EXPECT_EQ(EACCES, err_proc_read);
                }

                /* Tests PTRACE_ATTACH on the parent. */
                ret = ptrace(PTRACE_ATTACH, parent, NULL, 0);
                if (can_trace_parent) {
                        EXPECT_EQ(0, ret);
                } else {
                        EXPECT_EQ(-1, ret);
                        EXPECT_EQ(EPERM, errno);
                }
                if (ret == 0) {
                        ASSERT_EQ(parent, waitpid(parent, &status, 0));
                        ASSERT_EQ(1, WIFSTOPPED(status));
                        ASSERT_EQ(0, ptrace(PTRACE_DETACH, parent, NULL, 0));
                }

                /* Tests child PTRACE_TRACEME. */
                ret = ptrace(PTRACE_TRACEME);
                if (can_trace_child) {
                        EXPECT_EQ(0, ret);
                } else {
                        EXPECT_EQ(-1, ret);
                        EXPECT_EQ(EPERM, errno);
                }

                /*
                 * Signals that the PTRACE_ATTACH test is done and the
                 * PTRACE_TRACEME test is ongoing.
                 */
                ASSERT_EQ(1, write(pipe_child[1], ".", 1));

                if (can_trace_child) {
                        ASSERT_EQ(0, raise(SIGSTOP));
                }

                /* Waits for the parent PTRACE_ATTACH test. */
                ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
                _exit(_metadata->exit_code);
                return;
        }

        ASSERT_EQ(0, close(pipe_child[1]));
        ASSERT_EQ(0, close(pipe_parent[0]));
        if (variant->domain_parent)
                create_domain(_metadata);

        /* Signals that the parent is in a domain, if any. */
        ASSERT_EQ(1, write(pipe_parent[1], ".", 1));

        /*
         * Waits for the child to test PTRACE_ATTACH on the parent and start
         * testing PTRACE_TRACEME.
         */
        ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));

        /* Tests child PTRACE_TRACEME. */
        if (can_trace_child) {
                ASSERT_EQ(child, waitpid(child, &status, 0));
                ASSERT_EQ(1, WIFSTOPPED(status));
                ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
        } else {
                /* The child should not be traced by the parent. */
                EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0));
                EXPECT_EQ(ESRCH, errno);
        }

        /* Tests PTRACE_MODE_READ on the child. */
        err_proc_read = test_ptrace_read(child);
        if (can_read_child) {
                EXPECT_EQ(0, err_proc_read);
        } else {
                EXPECT_EQ(EACCES, err_proc_read);
        }

        /* Tests PTRACE_ATTACH on the child. */
        ret = ptrace(PTRACE_ATTACH, child, NULL, 0);
        if (can_trace_child) {
                EXPECT_EQ(0, ret);
        } else {
                EXPECT_EQ(-1, ret);
                EXPECT_EQ(EPERM, errno);
        }

        if (ret == 0) {
                ASSERT_EQ(child, waitpid(child, &status, 0));
                ASSERT_EQ(1, WIFSTOPPED(status));
                ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
        }

        /* Signals that the parent PTRACE_ATTACH test is done. */
        ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
        ASSERT_EQ(child, waitpid(child, &status, 0));

        if (WIFSIGNALED(status) || !WIFEXITED(status) ||
            WEXITSTATUS(status) != EXIT_SUCCESS)
                _metadata->exit_code = KSFT_FAIL;
}

static int matches_log_ptrace(struct __test_metadata *const _metadata,
                              int audit_fd, const pid_t opid)
{
        static const char log_template[] = REGEX_LANDLOCK_PREFIX
                " blockers=ptrace opid=%d ocomm=\"ptrace_test\"$";
        char log_match[sizeof(log_template) + 10];
        int log_match_len;

        log_match_len =
                snprintf(log_match, sizeof(log_match), log_template, opid);
        if (log_match_len > sizeof(log_match))
                return -E2BIG;

        return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match,
                                  NULL);
}

FIXTURE(audit)
{
        struct audit_filter audit_filter;
        int audit_fd;
};

FIXTURE_SETUP(audit)
{
        disable_caps(_metadata);
        set_cap(_metadata, CAP_AUDIT_CONTROL);
        self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
        EXPECT_LE(0, self->audit_fd);
        clear_cap(_metadata, CAP_AUDIT_CONTROL);
}

FIXTURE_TEARDOWN_PARENT(audit)
{
        EXPECT_EQ(0, audit_cleanup(-1, NULL));
}

/* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */
TEST_F(audit, trace)
{
        pid_t child;
        int status;
        int pipe_child[2], pipe_parent[2];
        int yama_ptrace_scope;
        char buf_parent;
        struct audit_records records;

        /* Makes sure there is no superfluous logged records. */
        EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
        EXPECT_EQ(0, records.access);
        EXPECT_EQ(0, records.domain);

        yama_ptrace_scope = get_yama_ptrace_scope();
        ASSERT_LE(0, yama_ptrace_scope);

        if (yama_ptrace_scope > YAMA_SCOPE_DISABLED)
                TH_LOG("Incomplete tests due to Yama restrictions (scope %d)",
                       yama_ptrace_scope);

        /*
         * Removes all effective and permitted capabilities to not interfere
         * with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS.
         */
        drop_caps(_metadata);

        ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
        ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));

        child = fork();
        ASSERT_LE(0, child);
        if (child == 0) {
                char buf_child;

                ASSERT_EQ(0, close(pipe_parent[1]));
                ASSERT_EQ(0, close(pipe_child[0]));

                /* Waits for the parent to be in a domain, if any. */
                ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));

                /* Tests child PTRACE_TRACEME. */
                EXPECT_EQ(-1, ptrace(PTRACE_TRACEME));
                EXPECT_EQ(EPERM, errno);
                /* We should see the child process. */
                EXPECT_EQ(0, matches_log_ptrace(_metadata, self->audit_fd,
                                                getpid()));

                EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
                EXPECT_EQ(0, records.access);
                /* Checks for a domain creation. */
                EXPECT_EQ(1, records.domain);

                /*
                 * Signals that the PTRACE_ATTACH test is done and the
                 * PTRACE_TRACEME test is ongoing.
                 */
                ASSERT_EQ(1, write(pipe_child[1], ".", 1));

                /* Waits for the parent PTRACE_ATTACH test. */
                ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
                _exit(_metadata->exit_code);
                return;
        }

        ASSERT_EQ(0, close(pipe_child[1]));
        ASSERT_EQ(0, close(pipe_parent[0]));
        create_domain(_metadata);

        /* Signals that the parent is in a domain. */
        ASSERT_EQ(1, write(pipe_parent[1], ".", 1));

        /*
         * Waits for the child to test PTRACE_ATTACH on the parent and start
         * testing PTRACE_TRACEME.
         */
        ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));

        /* The child should not be traced by the parent. */
        EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0));
        EXPECT_EQ(ESRCH, errno);

        /* Tests PTRACE_ATTACH on the child. */
        EXPECT_EQ(-1, ptrace(PTRACE_ATTACH, child, NULL, 0));
        EXPECT_EQ(EPERM, errno);
        EXPECT_EQ(0, matches_log_ptrace(_metadata, self->audit_fd, child));

        /* Signals that the parent PTRACE_ATTACH test is done. */
        ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
        ASSERT_EQ(child, waitpid(child, &status, 0));
        if (WIFSIGNALED(status) || !WIFEXITED(status) ||
            WEXITSTATUS(status) != EXIT_SUCCESS)
                _metadata->exit_code = KSFT_FAIL;

        /* Makes sure there is no superfluous logged records. */
        EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
        EXPECT_EQ(0, records.access);
        EXPECT_EQ(0, records.domain);
}

TEST_HARNESS_MAIN