// SPDX-License-Identifier: GPL-2.0 /* * Landlock tests - Ptrace * * Copyright © 2017-2020 Mickaël Salaün * Copyright © 2019-2020 ANSSI * Copyright © 2024-2025 Microsoft Corporation */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include "audit.h" #include "common.h" #include "trace.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); } /* Trace tests */ /* clang-format off */ FIXTURE(trace_ptrace) { /* clang-format on */ int tracefs_ok; }; FIXTURE_SETUP(trace_ptrace) { int ret; set_cap(_metadata, CAP_SYS_ADMIN); ASSERT_EQ(0, unshare(CLONE_NEWNS)); ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); ret = tracefs_fixture_setup(); if (ret) { clear_cap(_metadata, CAP_SYS_ADMIN); self->tracefs_ok = 0; SKIP(return, "tracefs not available"); } self->tracefs_ok = 1; ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, true)); ASSERT_EQ(0, tracefs_clear()); clear_cap(_metadata, CAP_SYS_ADMIN); } FIXTURE_TEARDOWN(trace_ptrace) { if (!self->tracefs_ok) return; set_cap(_metadata, CAP_SYS_ADMIN); tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, false); tracefs_fixture_teardown(); clear_cap(_metadata, CAP_SYS_ADMIN); } /* clang-format off */ FIXTURE_VARIANT(trace_ptrace) { /* clang-format on */ bool sandbox; bool sandbox_target; int expect_denied; }; /* Denied: sandboxed child ptraces unsandboxed parent (tracee_domain=0). */ /* clang-format off */ FIXTURE_VARIANT_ADD(trace_ptrace, denied) { /* clang-format on */ .sandbox = true, .sandbox_target = false, .expect_denied = 1, }; /* * Denied: sandboxed child ptraces a sandboxed parent, so the tracee is in a * domain and tracee_domain= is non-zero. */ /* clang-format off */ FIXTURE_VARIANT_ADD(trace_ptrace, denied_scoped_target) { /* clang-format on */ .sandbox = true, .sandbox_target = true, .expect_denied = 1, }; /* Allowed: unsandboxed child uses PTRACE_TRACEME. */ /* clang-format off */ FIXTURE_VARIANT_ADD(trace_ptrace, allowed) { /* clang-format on */ .sandbox = false, .sandbox_target = false, .expect_denied = 0, }; TEST_F(trace_ptrace, deny_ptrace) { char *buf, field[64], expected_pid[16]; int count, status; pid_t child, parent; if (!self->tracefs_ok) SKIP(return, "tracefs not available"); parent = getpid(); /* * Set a known comm so the denied variant can verify both the trace line * task name and the tracee_comm= field. */ prctl(PR_SET_NAME, "ll_trace_test"); /* * For the non-zero tracee_domain case, sandbox the parent (the tracee) * before forking. The child inherits that domain and adds its own * layer, so the child (tracer) is not an ancestor of the tracee and the * ptrace is still denied, with tracee_domain= naming the parent's * domain. */ if (variant->sandbox_target) create_domain(_metadata); child = fork(); ASSERT_LE(0, child); if (child == 0) { if (variant->sandbox) { struct landlock_ruleset_attr ruleset_attr = { .scoped = LANDLOCK_SCOPE_SIGNAL, }; int ruleset_fd; /* * Any scope creates a domain. Ptrace denial checks * domain ancestry, not specific flags. */ ruleset_fd = landlock_create_ruleset( &ruleset_attr, sizeof(ruleset_attr), 0); if (ruleset_fd < 0) _exit(1); prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); if (landlock_restrict_self(ruleset_fd, 0)) { close(ruleset_fd); _exit(1); } close(ruleset_fd); /* PTRACE_ATTACH on unsandboxed parent: denied. */ if (ptrace(PTRACE_ATTACH, parent, NULL, NULL) == 0) { ptrace(PTRACE_DETACH, parent, NULL, NULL); _exit(2); } if (errno != EPERM) _exit(3); } else { /* No sandbox: ptrace should succeed. */ if (ptrace(PTRACE_TRACEME) != 0) _exit(1); } _exit(0); } ASSERT_EQ(child, waitpid(child, &status, 0)); ASSERT_TRUE(WIFEXITED(status)); EXPECT_EQ(0, WEXITSTATUS(status)); buf = tracefs_read_buf(); ASSERT_NE(NULL, buf); count = tracefs_count_matches(buf, REGEX_DENY_PTRACE("ll_trace_test")); if (variant->expect_denied) { EXPECT_EQ(variant->expect_denied, count) { TH_LOG("Expected deny_ptrace event, got %d\n%s", count, buf); } /* Verify tracee_pid is the parent's TGID. */ snprintf(expected_pid, sizeof(expected_pid), "%d", parent); ASSERT_EQ(0, tracefs_extract_field( buf, REGEX_DENY_PTRACE("ll_trace_test"), "tracee_pid", field, sizeof(field))); EXPECT_STREQ(expected_pid, field); /* Verify tracee_comm matches prctl(PR_SET_NAME). */ ASSERT_EQ(0, tracefs_extract_field( buf, REGEX_DENY_PTRACE("ll_trace_test"), "tracee_comm", field, sizeof(field))); EXPECT_STREQ("ll_trace_test", field); /* * Verify tracee_domain: 0 when the tracee is unsandboxed, * non-zero when the tracee is in a domain. */ ASSERT_EQ(0, tracefs_extract_field( buf, REGEX_DENY_PTRACE("ll_trace_test"), "tracee_domain", field, sizeof(field))); EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0) { TH_LOG("Unexpected tracee_domain=%s", field); } } else { EXPECT_EQ(0, count) { TH_LOG("Expected 0 deny_ptrace events, got %d\n%s", count, buf); } } free(buf); } /* clang-format off */ FIXTURE(trace_ptrace_traceme) { /* clang-format on */ int tracefs_ok; }; FIXTURE_SETUP(trace_ptrace_traceme) { int ret; set_cap(_metadata, CAP_SYS_ADMIN); ASSERT_EQ(0, unshare(CLONE_NEWNS)); ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); ret = tracefs_fixture_setup(); if (ret) { clear_cap(_metadata, CAP_SYS_ADMIN); self->tracefs_ok = 0; SKIP(return, "tracefs not available"); } self->tracefs_ok = 1; ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, true)); ASSERT_EQ(0, tracefs_clear()); clear_cap(_metadata, CAP_SYS_ADMIN); } FIXTURE_TEARDOWN(trace_ptrace_traceme) { if (!self->tracefs_ok) return; set_cap(_metadata, CAP_SYS_ADMIN); tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, false); tracefs_fixture_teardown(); clear_cap(_metadata, CAP_SYS_ADMIN); } /* clang-format off */ FIXTURE_VARIANT(trace_ptrace_traceme) { /* clang-format on */ bool sandbox_tracer; bool sandbox_tracee; int expect_denied; }; /* * Denied: a sandboxed tracer cannot trace the unsandboxed child that asked to * be traced with PTRACE_TRACEME (tracee_domain=0). */ /* clang-format off */ FIXTURE_VARIANT_ADD(trace_ptrace_traceme, denied) { /* clang-format on */ .sandbox_tracer = true, .sandbox_tracee = false, .expect_denied = 1, }; /* * Denied: a sandboxed child in its own domain asks to be traced by a tracer in * an unrelated domain, so the tracee is in a domain and tracee_domain= is * non-zero. */ /* clang-format off */ FIXTURE_VARIANT_ADD(trace_ptrace_traceme, denied_scoped_tracee) { /* clang-format on */ .sandbox_tracer = true, .sandbox_tracee = true, .expect_denied = 1, }; /* Allowed: unsandboxed child uses PTRACE_TRACEME with an unsandboxed tracer. */ /* clang-format off */ FIXTURE_VARIANT_ADD(trace_ptrace_traceme, allowed) { /* clang-format on */ .sandbox_tracer = false, .sandbox_tracee = false, .expect_denied = 0, }; TEST_F(trace_ptrace_traceme, deny_ptrace) { char *buf, field[64], expected_pid[16]; int count, status, sync_pipe[2]; pid_t child; if (!self->tracefs_ok) SKIP(return, "tracefs not available"); /* * Set a known comm so the denied variant can verify both the trace line * task name and the tracee_comm= field. The tracee is the current * (child) task for PTRACE_TRACEME, so the child inherits this name. */ prctl(PR_SET_NAME, "ll_trace_test"); ASSERT_EQ(0, pipe2(sync_pipe, O_CLOEXEC)); child = fork(); ASSERT_LE(0, child); if (child == 0) { char c; close(sync_pipe[1]); /* * The tracee is the current task; for the non-zero * tracee_domain case it sandboxes itself in its own domain, * unrelated to the tracer's domain, so PTRACE_TRACEME is still * denied and tracee_domain= names the child's own domain. */ if (variant->sandbox_tracee) create_domain(_metadata); /* Waits for the tracer (parent) to enter its domain, if any. */ if (read(sync_pipe[0], &c, 1) != 1) _exit(1); close(sync_pipe[0]); if (variant->expect_denied) { if (ptrace(PTRACE_TRACEME) == 0) _exit(2); if (errno != EPERM) _exit(3); } else { if (ptrace(PTRACE_TRACEME) != 0) _exit(4); /* Lets the tracer reap the trace-stop and detach. */ raise(SIGSTOP); } _exit(0); } close(sync_pipe[0]); /* * For a denial, the proposed tracer must be in a domain that is not an * ancestor of the tracee's domain. Sandboxing the parent after the * fork gives it a domain unrelated to the child. */ if (variant->sandbox_tracer) create_domain(_metadata); /* Signals the child that the tracer is in its domain, if any. */ ASSERT_EQ(1, write(sync_pipe[1], ".", 1)); close(sync_pipe[1]); if (!variant->expect_denied) { /* PTRACE_TRACEME succeeded: reap the SIGSTOP and detach. */ ASSERT_EQ(child, waitpid(child, &status, WUNTRACED)); ASSERT_TRUE(WIFSTOPPED(status)); ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0)); } ASSERT_EQ(child, waitpid(child, &status, 0)); ASSERT_TRUE(WIFEXITED(status)); EXPECT_EQ(0, WEXITSTATUS(status)); buf = tracefs_read_buf(); ASSERT_NE(NULL, buf); count = tracefs_count_matches(buf, REGEX_DENY_PTRACE("ll_trace_test")); if (variant->expect_denied) { EXPECT_EQ(variant->expect_denied, count) { TH_LOG("Expected deny_ptrace event, got %d\n%s", count, buf); } /* Verify tracee_pid is the child's TGID (the traced task). */ snprintf(expected_pid, sizeof(expected_pid), "%d", child); ASSERT_EQ(0, tracefs_extract_field( buf, REGEX_DENY_PTRACE("ll_trace_test"), "tracee_pid", field, sizeof(field))); EXPECT_STREQ(expected_pid, field); /* * Verify tracee_domain: 0 when the tracee is unsandboxed, * non-zero when the tracee is in a domain. */ ASSERT_EQ(0, tracefs_extract_field( buf, REGEX_DENY_PTRACE("ll_trace_test"), "tracee_domain", field, sizeof(field))); EXPECT_EQ(variant->sandbox_tracee, strcmp("0", field) != 0) { TH_LOG("Unexpected tracee_domain=%s", field); } } else { EXPECT_EQ(0, count) { TH_LOG("Expected 0 deny_ptrace events, got %d\n%s", count, buf); } } free(buf); } TEST_HARNESS_MAIN