| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Landlock tests - Abstract UNIX socket |
| * |
| * Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com> |
| */ |
| |
| #define _GNU_SOURCE |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <linux/landlock.h> |
| #include <sched.h> |
| #include <signal.h> |
| #include <stddef.h> |
| #include <sys/mount.h> |
| #include <sys/prctl.h> |
| #include <sys/socket.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <sys/un.h> |
| #include <sys/wait.h> |
| #include <unistd.h> |
| |
| #include "audit.h" |
| #include "common.h" |
| #include "scoped_common.h" |
| #include "trace.h" |
| |
| #define TRACE_TASK "scoped_abstract" |
| |
| /* Number of pending connections queue to be hold. */ |
| const short backlog = 10; |
| |
| static void create_fs_domain(struct __test_metadata *const _metadata) |
| { |
| int ruleset_fd; |
| struct landlock_ruleset_attr ruleset_attr = { |
| .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, |
| }; |
| |
| 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)); |
| } |
| |
| FIXTURE(scoped_domains) |
| { |
| struct service_fixture stream_address, dgram_address; |
| }; |
| |
| #include "scoped_base_variants.h" |
| |
| FIXTURE_SETUP(scoped_domains) |
| { |
| drop_caps(_metadata); |
| |
| memset(&self->stream_address, 0, sizeof(self->stream_address)); |
| memset(&self->dgram_address, 0, sizeof(self->dgram_address)); |
| set_unix_address(&self->stream_address, 0); |
| set_unix_address(&self->dgram_address, 1); |
| } |
| |
| FIXTURE_TEARDOWN(scoped_domains) |
| { |
| } |
| |
| /* |
| * Test unix_stream_connect() and unix_may_send() for a child connecting to its |
| * parent, when they have scoped domain or no domain. |
| */ |
| TEST_F(scoped_domains, connect_to_parent) |
| { |
| pid_t child; |
| bool can_connect_to_parent; |
| int status; |
| int pipe_parent[2]; |
| int stream_server, dgram_server; |
| |
| /* |
| * can_connect_to_parent is true if a child process can connect to its |
| * parent process. This depends on the child process not being isolated |
| * from the parent with a dedicated Landlock domain. |
| */ |
| can_connect_to_parent = !variant->domain_child; |
| |
| ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); |
| if (variant->domain_both) { |
| create_scoped_domain(_metadata, |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| if (!__test_passed(_metadata)) |
| return; |
| } |
| |
| child = fork(); |
| ASSERT_LE(0, child); |
| if (child == 0) { |
| int err; |
| int stream_client, dgram_client; |
| char buf_child; |
| |
| EXPECT_EQ(0, close(pipe_parent[1])); |
| if (variant->domain_child) |
| create_scoped_domain( |
| _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| stream_client = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_client); |
| dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_client); |
| |
| /* Waits for the server. */ |
| ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); |
| |
| err = connect(stream_client, &self->stream_address.unix_addr, |
| self->stream_address.unix_addr_len); |
| if (can_connect_to_parent) { |
| EXPECT_EQ(0, err); |
| } else { |
| EXPECT_EQ(-1, err); |
| EXPECT_EQ(EPERM, errno); |
| } |
| EXPECT_EQ(0, close(stream_client)); |
| |
| err = connect(dgram_client, &self->dgram_address.unix_addr, |
| self->dgram_address.unix_addr_len); |
| if (can_connect_to_parent) { |
| EXPECT_EQ(0, err); |
| } else { |
| EXPECT_EQ(-1, err); |
| EXPECT_EQ(EPERM, errno); |
| } |
| EXPECT_EQ(0, close(dgram_client)); |
| _exit(_metadata->exit_code); |
| return; |
| } |
| EXPECT_EQ(0, close(pipe_parent[0])); |
| if (variant->domain_parent) |
| create_scoped_domain(_metadata, |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| stream_server = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_server); |
| dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_server); |
| ASSERT_EQ(0, bind(stream_server, &self->stream_address.unix_addr, |
| self->stream_address.unix_addr_len)); |
| ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr, |
| self->dgram_address.unix_addr_len)); |
| ASSERT_EQ(0, listen(stream_server, backlog)); |
| |
| /* Signals to child that the parent is listening. */ |
| ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); |
| |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| EXPECT_EQ(0, close(stream_server)); |
| EXPECT_EQ(0, close(dgram_server)); |
| |
| if (WIFSIGNALED(status) || !WIFEXITED(status) || |
| WEXITSTATUS(status) != EXIT_SUCCESS) |
| _metadata->exit_code = KSFT_FAIL; |
| } |
| |
| /* |
| * Test unix_stream_connect() and unix_may_send() for a parent connecting to |
| * its child, when they have scoped domain or no domain. |
| */ |
| TEST_F(scoped_domains, connect_to_child) |
| { |
| pid_t child; |
| bool can_connect_to_child; |
| int err_stream, err_dgram, errno_stream, errno_dgram, status; |
| int pipe_child[2], pipe_parent[2]; |
| char buf; |
| int stream_client, dgram_client; |
| |
| /* |
| * can_connect_to_child is true if a parent process can connect to its |
| * child process. The parent process is not isolated from the child |
| * with a dedicated Landlock domain. |
| */ |
| can_connect_to_child = !variant->domain_parent; |
| |
| ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); |
| ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); |
| if (variant->domain_both) { |
| create_scoped_domain(_metadata, |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| if (!__test_passed(_metadata)) |
| return; |
| } |
| |
| child = fork(); |
| ASSERT_LE(0, child); |
| if (child == 0) { |
| int stream_server, dgram_server; |
| |
| EXPECT_EQ(0, close(pipe_parent[1])); |
| EXPECT_EQ(0, close(pipe_child[0])); |
| if (variant->domain_child) |
| create_scoped_domain( |
| _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| /* Waits for the parent to be in a domain, if any. */ |
| ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); |
| |
| stream_server = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_server); |
| dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_server); |
| ASSERT_EQ(0, |
| bind(stream_server, &self->stream_address.unix_addr, |
| self->stream_address.unix_addr_len)); |
| ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr, |
| self->dgram_address.unix_addr_len)); |
| ASSERT_EQ(0, listen(stream_server, backlog)); |
| |
| /* Signals to the parent that child is listening. */ |
| ASSERT_EQ(1, write(pipe_child[1], ".", 1)); |
| |
| /* Waits to connect. */ |
| ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); |
| EXPECT_EQ(0, close(stream_server)); |
| EXPECT_EQ(0, close(dgram_server)); |
| _exit(_metadata->exit_code); |
| return; |
| } |
| EXPECT_EQ(0, close(pipe_child[1])); |
| EXPECT_EQ(0, close(pipe_parent[0])); |
| |
| if (variant->domain_parent) |
| create_scoped_domain(_metadata, |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| /* Signals that the parent is in a domain, if any. */ |
| ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); |
| |
| stream_client = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_client); |
| dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_client); |
| |
| /* Waits for the child to listen */ |
| ASSERT_EQ(1, read(pipe_child[0], &buf, 1)); |
| err_stream = connect(stream_client, &self->stream_address.unix_addr, |
| self->stream_address.unix_addr_len); |
| errno_stream = errno; |
| err_dgram = connect(dgram_client, &self->dgram_address.unix_addr, |
| self->dgram_address.unix_addr_len); |
| errno_dgram = errno; |
| if (can_connect_to_child) { |
| EXPECT_EQ(0, err_stream); |
| EXPECT_EQ(0, err_dgram); |
| } else { |
| EXPECT_EQ(-1, err_stream); |
| EXPECT_EQ(-1, err_dgram); |
| EXPECT_EQ(EPERM, errno_stream); |
| EXPECT_EQ(EPERM, errno_dgram); |
| } |
| ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); |
| EXPECT_EQ(0, close(stream_client)); |
| EXPECT_EQ(0, close(dgram_client)); |
| |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| if (WIFSIGNALED(status) || !WIFEXITED(status) || |
| WEXITSTATUS(status) != EXIT_SUCCESS) |
| _metadata->exit_code = KSFT_FAIL; |
| } |
| |
| FIXTURE(scoped_audit) |
| { |
| struct service_fixture dgram_address; |
| struct audit_filter audit_filter; |
| int audit_fd; |
| }; |
| |
| FIXTURE_SETUP(scoped_audit) |
| { |
| disable_caps(_metadata); |
| |
| memset(&self->dgram_address, 0, sizeof(self->dgram_address)); |
| set_unix_address(&self->dgram_address, 1); |
| |
| set_cap(_metadata, CAP_AUDIT_CONTROL); |
| self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); |
| EXPECT_LE(0, self->audit_fd); |
| drop_caps(_metadata); |
| } |
| |
| FIXTURE_TEARDOWN_PARENT(scoped_audit) |
| { |
| EXPECT_EQ(0, audit_cleanup(-1, NULL)); |
| } |
| |
| FIXTURE_VARIANT(scoped_audit) |
| { |
| const __u64 scoped; |
| const __u64 quiet_scoped; |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(scoped_audit, no_quiet) |
| { |
| /* clang-format on */ |
| .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, |
| .quiet_scoped = 0, |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(scoped_audit, quiet_abstract_socket) |
| { |
| /* clang-format on */ |
| .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, |
| .quiet_scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(scoped_audit, quiet_abstract_socket_2) |
| { |
| /* clang-format on */ |
| .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL, |
| .quiet_scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | |
| LANDLOCK_SCOPE_SIGNAL, |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(scoped_audit, quiet_unrelated) |
| { |
| /* clang-format on */ |
| .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL, |
| .quiet_scoped = LANDLOCK_SCOPE_SIGNAL, |
| }; |
| |
| /* python -c 'print(b"\0selftests-landlock-abstract-unix-".hex().upper())' */ |
| #define ABSTRACT_SOCKET_PATH_PREFIX \ |
| "0073656C6674657374732D6C616E646C6F636B2D61627374726163742D756E69782D" |
| |
| /* |
| * Simpler version of scoped_domains.connect_to_child, but with audit tests. |
| */ |
| TEST_F(scoped_audit, connect_to_child) |
| { |
| pid_t child; |
| int err_dgram, status; |
| int pipe_child[2], pipe_parent[2]; |
| char buf; |
| int dgram_client; |
| struct audit_records records; |
| int ruleset_fd; |
| const struct landlock_ruleset_attr ruleset_attr = { |
| .scoped = variant->scoped, |
| .quiet_scoped = variant->quiet_scoped, |
| }; |
| bool should_audit = |
| !(variant->quiet_scoped & LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| /* 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); |
| |
| 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) { |
| int dgram_server; |
| |
| EXPECT_EQ(0, close(pipe_parent[1])); |
| EXPECT_EQ(0, close(pipe_child[0])); |
| |
| /* Waits for the parent to be in a domain. */ |
| ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); |
| |
| dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_server); |
| ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr, |
| self->dgram_address.unix_addr_len)); |
| |
| /* Signals to the parent that child is listening. */ |
| ASSERT_EQ(1, write(pipe_child[1], ".", 1)); |
| |
| /* Waits to connect. */ |
| ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); |
| EXPECT_EQ(0, close(dgram_server)); |
| _exit(_metadata->exit_code); |
| return; |
| } |
| EXPECT_EQ(0, close(pipe_child[1])); |
| EXPECT_EQ(0, close(pipe_parent[0])); |
| |
| ruleset_fd = |
| landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); |
| ASSERT_LE(0, ruleset_fd) |
| { |
| TH_LOG("Failed to create a ruleset: %s", strerror(errno)); |
| } |
| enforce_ruleset(_metadata, ruleset_fd); |
| EXPECT_EQ(0, close(ruleset_fd)); |
| |
| /* Signals that the parent is in a domain, if any. */ |
| ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); |
| |
| dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_client); |
| |
| /* Waits for the child to listen */ |
| ASSERT_EQ(1, read(pipe_child[0], &buf, 1)); |
| err_dgram = connect(dgram_client, &self->dgram_address.unix_addr, |
| self->dgram_address.unix_addr_len); |
| EXPECT_EQ(-1, err_dgram); |
| EXPECT_EQ(EPERM, errno); |
| |
| if (should_audit) { |
| EXPECT_EQ( |
| 0, |
| audit_match_record( |
| self->audit_fd, AUDIT_LANDLOCK_ACCESS, |
| REGEX_LANDLOCK_PREFIX |
| " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX |
| "[0-9A-F]\\+$", |
| NULL)); |
| } |
| |
| /* No other logs */ |
| EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); |
| EXPECT_EQ(0, records.access); |
| |
| ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); |
| EXPECT_EQ(0, close(dgram_client)); |
| |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| if (WIFSIGNALED(status) || !WIFEXITED(status) || |
| WEXITSTATUS(status) != EXIT_SUCCESS) |
| _metadata->exit_code = KSFT_FAIL; |
| } |
| |
| FIXTURE(scoped_vs_unscoped) |
| { |
| struct service_fixture parent_stream_address, parent_dgram_address, |
| child_stream_address, child_dgram_address; |
| }; |
| |
| #include "scoped_multiple_domain_variants.h" |
| |
| FIXTURE_SETUP(scoped_vs_unscoped) |
| { |
| drop_caps(_metadata); |
| |
| memset(&self->parent_stream_address, 0, |
| sizeof(self->parent_stream_address)); |
| set_unix_address(&self->parent_stream_address, 0); |
| memset(&self->parent_dgram_address, 0, |
| sizeof(self->parent_dgram_address)); |
| set_unix_address(&self->parent_dgram_address, 1); |
| memset(&self->child_stream_address, 0, |
| sizeof(self->child_stream_address)); |
| set_unix_address(&self->child_stream_address, 2); |
| memset(&self->child_dgram_address, 0, |
| sizeof(self->child_dgram_address)); |
| set_unix_address(&self->child_dgram_address, 3); |
| } |
| |
| FIXTURE_TEARDOWN(scoped_vs_unscoped) |
| { |
| } |
| |
| /* |
| * Test unix_stream_connect and unix_may_send for parent, child and |
| * grand child processes when they can have scoped or non-scoped domains. |
| */ |
| TEST_F(scoped_vs_unscoped, unix_scoping) |
| { |
| pid_t child; |
| int status; |
| bool can_connect_to_parent, can_connect_to_child; |
| int pipe_parent[2]; |
| int stream_server_parent, dgram_server_parent; |
| |
| can_connect_to_child = (variant->domain_grand_child != SCOPE_SANDBOX); |
| can_connect_to_parent = (can_connect_to_child && |
| (variant->domain_children != SCOPE_SANDBOX)); |
| |
| ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); |
| |
| if (variant->domain_all == OTHER_SANDBOX) |
| create_fs_domain(_metadata); |
| else if (variant->domain_all == SCOPE_SANDBOX) |
| create_scoped_domain(_metadata, |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| child = fork(); |
| ASSERT_LE(0, child); |
| if (child == 0) { |
| int stream_server_child, dgram_server_child; |
| int pipe_child[2]; |
| pid_t grand_child; |
| |
| ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); |
| |
| if (variant->domain_children == OTHER_SANDBOX) |
| create_fs_domain(_metadata); |
| else if (variant->domain_children == SCOPE_SANDBOX) |
| create_scoped_domain( |
| _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| grand_child = fork(); |
| ASSERT_LE(0, grand_child); |
| if (grand_child == 0) { |
| char buf; |
| int stream_err, dgram_err, stream_errno, dgram_errno; |
| int stream_client, dgram_client; |
| |
| EXPECT_EQ(0, close(pipe_parent[1])); |
| EXPECT_EQ(0, close(pipe_child[1])); |
| |
| if (variant->domain_grand_child == OTHER_SANDBOX) |
| create_fs_domain(_metadata); |
| else if (variant->domain_grand_child == SCOPE_SANDBOX) |
| create_scoped_domain( |
| _metadata, |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| stream_client = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_client); |
| dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_client); |
| |
| ASSERT_EQ(1, read(pipe_child[0], &buf, 1)); |
| stream_err = connect( |
| stream_client, |
| &self->child_stream_address.unix_addr, |
| self->child_stream_address.unix_addr_len); |
| stream_errno = errno; |
| dgram_err = connect( |
| dgram_client, |
| &self->child_dgram_address.unix_addr, |
| self->child_dgram_address.unix_addr_len); |
| dgram_errno = errno; |
| if (can_connect_to_child) { |
| EXPECT_EQ(0, stream_err); |
| EXPECT_EQ(0, dgram_err); |
| } else { |
| EXPECT_EQ(-1, stream_err); |
| EXPECT_EQ(-1, dgram_err); |
| EXPECT_EQ(EPERM, stream_errno); |
| EXPECT_EQ(EPERM, dgram_errno); |
| } |
| |
| EXPECT_EQ(0, close(stream_client)); |
| stream_client = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_client); |
| /* Datagram sockets can "reconnect". */ |
| |
| ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); |
| stream_err = connect( |
| stream_client, |
| &self->parent_stream_address.unix_addr, |
| self->parent_stream_address.unix_addr_len); |
| stream_errno = errno; |
| dgram_err = connect( |
| dgram_client, |
| &self->parent_dgram_address.unix_addr, |
| self->parent_dgram_address.unix_addr_len); |
| dgram_errno = errno; |
| if (can_connect_to_parent) { |
| EXPECT_EQ(0, stream_err); |
| EXPECT_EQ(0, dgram_err); |
| } else { |
| EXPECT_EQ(-1, stream_err); |
| EXPECT_EQ(-1, dgram_err); |
| EXPECT_EQ(EPERM, stream_errno); |
| EXPECT_EQ(EPERM, dgram_errno); |
| } |
| EXPECT_EQ(0, close(stream_client)); |
| EXPECT_EQ(0, close(dgram_client)); |
| |
| _exit(_metadata->exit_code); |
| return; |
| } |
| EXPECT_EQ(0, close(pipe_child[0])); |
| if (variant->domain_child == OTHER_SANDBOX) |
| create_fs_domain(_metadata); |
| else if (variant->domain_child == SCOPE_SANDBOX) |
| create_scoped_domain( |
| _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| stream_server_child = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_server_child); |
| dgram_server_child = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_server_child); |
| |
| ASSERT_EQ(0, bind(stream_server_child, |
| &self->child_stream_address.unix_addr, |
| self->child_stream_address.unix_addr_len)); |
| ASSERT_EQ(0, bind(dgram_server_child, |
| &self->child_dgram_address.unix_addr, |
| self->child_dgram_address.unix_addr_len)); |
| ASSERT_EQ(0, listen(stream_server_child, backlog)); |
| |
| ASSERT_EQ(1, write(pipe_child[1], ".", 1)); |
| ASSERT_EQ(grand_child, waitpid(grand_child, &status, 0)); |
| EXPECT_EQ(0, close(stream_server_child)); |
| EXPECT_EQ(0, close(dgram_server_child)); |
| return; |
| } |
| EXPECT_EQ(0, close(pipe_parent[0])); |
| |
| if (variant->domain_parent == OTHER_SANDBOX) |
| create_fs_domain(_metadata); |
| else if (variant->domain_parent == SCOPE_SANDBOX) |
| create_scoped_domain(_metadata, |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| stream_server_parent = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_server_parent); |
| dgram_server_parent = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_server_parent); |
| ASSERT_EQ(0, bind(stream_server_parent, |
| &self->parent_stream_address.unix_addr, |
| self->parent_stream_address.unix_addr_len)); |
| ASSERT_EQ(0, bind(dgram_server_parent, |
| &self->parent_dgram_address.unix_addr, |
| self->parent_dgram_address.unix_addr_len)); |
| |
| ASSERT_EQ(0, listen(stream_server_parent, backlog)); |
| |
| ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| EXPECT_EQ(0, close(stream_server_parent)); |
| EXPECT_EQ(0, close(dgram_server_parent)); |
| |
| if (WIFSIGNALED(status) || !WIFEXITED(status) || |
| WEXITSTATUS(status) != EXIT_SUCCESS) |
| _metadata->exit_code = KSFT_FAIL; |
| } |
| |
| FIXTURE(outside_socket) |
| { |
| struct service_fixture address, transit_address; |
| }; |
| |
| FIXTURE_VARIANT(outside_socket) |
| { |
| const bool child_socket; |
| const int type; |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(outside_socket, allow_dgram_child) { |
| /* clang-format on */ |
| .child_socket = true, |
| .type = SOCK_DGRAM, |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(outside_socket, deny_dgram_server) { |
| /* clang-format on */ |
| .child_socket = false, |
| .type = SOCK_DGRAM, |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(outside_socket, allow_stream_child) { |
| /* clang-format on */ |
| .child_socket = true, |
| .type = SOCK_STREAM, |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(outside_socket, deny_stream_server) { |
| /* clang-format on */ |
| .child_socket = false, |
| .type = SOCK_STREAM, |
| }; |
| |
| FIXTURE_SETUP(outside_socket) |
| { |
| drop_caps(_metadata); |
| |
| memset(&self->transit_address, 0, sizeof(self->transit_address)); |
| set_unix_address(&self->transit_address, 0); |
| memset(&self->address, 0, sizeof(self->address)); |
| set_unix_address(&self->address, 1); |
| } |
| |
| FIXTURE_TEARDOWN(outside_socket) |
| { |
| } |
| |
| /* |
| * Test unix_stream_connect and unix_may_send for parent and child processes |
| * when connecting socket has different domain than the process using it. |
| */ |
| TEST_F(outside_socket, socket_with_different_domain) |
| { |
| pid_t child; |
| int err, status; |
| int pipe_child[2], pipe_parent[2]; |
| char buf_parent; |
| int server_socket; |
| |
| 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) { |
| int client_socket; |
| char buf_child; |
| |
| EXPECT_EQ(0, close(pipe_parent[1])); |
| EXPECT_EQ(0, close(pipe_child[0])); |
| |
| /* Client always has a domain. */ |
| create_scoped_domain(_metadata, |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| if (variant->child_socket) { |
| int data_socket, passed_socket, stream_server; |
| |
| passed_socket = socket(AF_UNIX, variant->type, 0); |
| ASSERT_LE(0, passed_socket); |
| stream_server = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_server); |
| ASSERT_EQ(0, bind(stream_server, |
| &self->transit_address.unix_addr, |
| self->transit_address.unix_addr_len)); |
| ASSERT_EQ(0, listen(stream_server, backlog)); |
| ASSERT_EQ(1, write(pipe_child[1], ".", 1)); |
| data_socket = accept(stream_server, NULL, NULL); |
| ASSERT_LE(0, data_socket); |
| ASSERT_EQ(0, send_fd(data_socket, passed_socket)); |
| EXPECT_EQ(0, close(passed_socket)); |
| EXPECT_EQ(0, close(stream_server)); |
| } |
| |
| client_socket = socket(AF_UNIX, variant->type, 0); |
| ASSERT_LE(0, client_socket); |
| |
| /* Waits for parent signal for connection. */ |
| ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); |
| err = connect(client_socket, &self->address.unix_addr, |
| self->address.unix_addr_len); |
| if (variant->child_socket) { |
| EXPECT_EQ(0, err); |
| } else { |
| EXPECT_EQ(-1, err); |
| EXPECT_EQ(EPERM, errno); |
| } |
| EXPECT_EQ(0, close(client_socket)); |
| _exit(_metadata->exit_code); |
| return; |
| } |
| EXPECT_EQ(0, close(pipe_child[1])); |
| EXPECT_EQ(0, close(pipe_parent[0])); |
| |
| if (variant->child_socket) { |
| int client_child = socket(AF_UNIX, SOCK_STREAM, 0); |
| |
| ASSERT_LE(0, client_child); |
| ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1)); |
| ASSERT_EQ(0, connect(client_child, |
| &self->transit_address.unix_addr, |
| self->transit_address.unix_addr_len)); |
| server_socket = recv_fd(client_child); |
| EXPECT_EQ(0, close(client_child)); |
| } else { |
| server_socket = socket(AF_UNIX, variant->type, 0); |
| } |
| ASSERT_LE(0, server_socket); |
| |
| /* Server always has a domain. */ |
| create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| ASSERT_EQ(0, bind(server_socket, &self->address.unix_addr, |
| self->address.unix_addr_len)); |
| if (variant->type == SOCK_STREAM) |
| ASSERT_EQ(0, listen(server_socket, backlog)); |
| |
| /* Signals to child that the parent is listening. */ |
| ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); |
| |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| EXPECT_EQ(0, close(server_socket)); |
| |
| if (WIFSIGNALED(status) || !WIFEXITED(status) || |
| WEXITSTATUS(status) != EXIT_SUCCESS) |
| _metadata->exit_code = KSFT_FAIL; |
| } |
| |
| static const char stream_path[] = TMP_DIR "/stream.sock"; |
| static const char dgram_path[] = TMP_DIR "/dgram.sock"; |
| |
| /* clang-format off */ |
| FIXTURE(various_address_sockets) {}; |
| /* clang-format on */ |
| |
| FIXTURE_VARIANT(various_address_sockets) |
| { |
| const int domain; |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_scoped_domain) { |
| /* clang-format on */ |
| .domain = SCOPE_SANDBOX, |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_other_domain) { |
| /* clang-format on */ |
| .domain = OTHER_SANDBOX, |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_no_domain) { |
| /* clang-format on */ |
| .domain = NO_SANDBOX, |
| }; |
| |
| FIXTURE_SETUP(various_address_sockets) |
| { |
| drop_caps(_metadata); |
| |
| umask(0077); |
| ASSERT_EQ(0, mkdir(TMP_DIR, 0700)); |
| } |
| |
| FIXTURE_TEARDOWN(various_address_sockets) |
| { |
| EXPECT_EQ(0, unlink(stream_path)); |
| EXPECT_EQ(0, unlink(dgram_path)); |
| EXPECT_EQ(0, rmdir(TMP_DIR)); |
| } |
| |
| TEST_F(various_address_sockets, scoped_pathname_sockets) |
| { |
| pid_t child; |
| int status; |
| char buf_child, buf_parent; |
| int pipe_parent[2]; |
| int unnamed_sockets[2]; |
| int stream_pathname_socket, dgram_pathname_socket, |
| stream_abstract_socket, dgram_abstract_socket, data_socket; |
| struct service_fixture stream_abstract_addr, dgram_abstract_addr; |
| struct sockaddr_un stream_pathname_addr = { |
| .sun_family = AF_UNIX, |
| }; |
| struct sockaddr_un dgram_pathname_addr = { |
| .sun_family = AF_UNIX, |
| }; |
| |
| /* Pathname address. */ |
| snprintf(stream_pathname_addr.sun_path, |
| sizeof(stream_pathname_addr.sun_path), "%s", stream_path); |
| snprintf(dgram_pathname_addr.sun_path, |
| sizeof(dgram_pathname_addr.sun_path), "%s", dgram_path); |
| |
| /* Abstract address. */ |
| memset(&stream_abstract_addr, 0, sizeof(stream_abstract_addr)); |
| set_unix_address(&stream_abstract_addr, 0); |
| memset(&dgram_abstract_addr, 0, sizeof(dgram_abstract_addr)); |
| set_unix_address(&dgram_abstract_addr, 1); |
| |
| /* Unnamed address for datagram socket. */ |
| ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_DGRAM, 0, unnamed_sockets)); |
| |
| ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); |
| |
| child = fork(); |
| ASSERT_LE(0, child); |
| if (child == 0) { |
| int err; |
| |
| EXPECT_EQ(0, close(pipe_parent[1])); |
| EXPECT_EQ(0, close(unnamed_sockets[1])); |
| |
| if (variant->domain == SCOPE_SANDBOX) |
| create_scoped_domain( |
| _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| else if (variant->domain == OTHER_SANDBOX) |
| create_fs_domain(_metadata); |
| |
| /* Waits for parent to listen. */ |
| ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); |
| EXPECT_EQ(0, close(pipe_parent[0])); |
| |
| /* Checks that we can send data through a datagram socket. */ |
| ASSERT_EQ(1, write(unnamed_sockets[0], "a", 1)); |
| EXPECT_EQ(0, close(unnamed_sockets[0])); |
| |
| /* Connects with pathname sockets. */ |
| stream_pathname_socket = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_pathname_socket); |
| ASSERT_EQ(0, |
| connect(stream_pathname_socket, &stream_pathname_addr, |
| sizeof(stream_pathname_addr))); |
| ASSERT_EQ(1, write(stream_pathname_socket, "b", 1)); |
| EXPECT_EQ(0, close(stream_pathname_socket)); |
| |
| /* Sends without connection. */ |
| dgram_pathname_socket = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_pathname_socket); |
| err = sendto(dgram_pathname_socket, "c", 1, 0, |
| &dgram_pathname_addr, sizeof(dgram_pathname_addr)); |
| EXPECT_EQ(1, err); |
| |
| /* Sends with connection. */ |
| ASSERT_EQ(0, |
| connect(dgram_pathname_socket, &dgram_pathname_addr, |
| sizeof(dgram_pathname_addr))); |
| ASSERT_EQ(1, write(dgram_pathname_socket, "d", 1)); |
| EXPECT_EQ(0, close(dgram_pathname_socket)); |
| |
| /* Connects with abstract sockets. */ |
| stream_abstract_socket = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_abstract_socket); |
| err = connect(stream_abstract_socket, |
| &stream_abstract_addr.unix_addr, |
| stream_abstract_addr.unix_addr_len); |
| if (variant->domain == SCOPE_SANDBOX) { |
| EXPECT_EQ(-1, err); |
| EXPECT_EQ(EPERM, errno); |
| } else { |
| EXPECT_EQ(0, err); |
| ASSERT_EQ(1, write(stream_abstract_socket, "e", 1)); |
| } |
| EXPECT_EQ(0, close(stream_abstract_socket)); |
| |
| /* Sends without connection. */ |
| dgram_abstract_socket = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_abstract_socket); |
| err = sendto(dgram_abstract_socket, "f", 1, 0, |
| &dgram_abstract_addr.unix_addr, |
| dgram_abstract_addr.unix_addr_len); |
| if (variant->domain == SCOPE_SANDBOX) { |
| EXPECT_EQ(-1, err); |
| EXPECT_EQ(EPERM, errno); |
| } else { |
| EXPECT_EQ(1, err); |
| } |
| |
| /* Sends with connection. */ |
| err = connect(dgram_abstract_socket, |
| &dgram_abstract_addr.unix_addr, |
| dgram_abstract_addr.unix_addr_len); |
| if (variant->domain == SCOPE_SANDBOX) { |
| EXPECT_EQ(-1, err); |
| EXPECT_EQ(EPERM, errno); |
| } else { |
| EXPECT_EQ(0, err); |
| ASSERT_EQ(1, write(dgram_abstract_socket, "g", 1)); |
| } |
| EXPECT_EQ(0, close(dgram_abstract_socket)); |
| |
| _exit(_metadata->exit_code); |
| return; |
| } |
| EXPECT_EQ(0, close(pipe_parent[0])); |
| EXPECT_EQ(0, close(unnamed_sockets[0])); |
| |
| /* Sets up pathname servers. */ |
| stream_pathname_socket = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_pathname_socket); |
| ASSERT_EQ(0, bind(stream_pathname_socket, &stream_pathname_addr, |
| sizeof(stream_pathname_addr))); |
| ASSERT_EQ(0, listen(stream_pathname_socket, backlog)); |
| |
| dgram_pathname_socket = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_pathname_socket); |
| ASSERT_EQ(0, bind(dgram_pathname_socket, &dgram_pathname_addr, |
| sizeof(dgram_pathname_addr))); |
| |
| /* Sets up abstract servers. */ |
| stream_abstract_socket = socket(AF_UNIX, SOCK_STREAM, 0); |
| ASSERT_LE(0, stream_abstract_socket); |
| ASSERT_EQ(0, |
| bind(stream_abstract_socket, &stream_abstract_addr.unix_addr, |
| stream_abstract_addr.unix_addr_len)); |
| |
| dgram_abstract_socket = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, dgram_abstract_socket); |
| ASSERT_EQ(0, bind(dgram_abstract_socket, &dgram_abstract_addr.unix_addr, |
| dgram_abstract_addr.unix_addr_len)); |
| ASSERT_EQ(0, listen(stream_abstract_socket, backlog)); |
| |
| ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); |
| EXPECT_EQ(0, close(pipe_parent[1])); |
| |
| /* Reads from unnamed socket. */ |
| ASSERT_EQ(1, read(unnamed_sockets[1], &buf_parent, sizeof(buf_parent))); |
| ASSERT_EQ('a', buf_parent); |
| EXPECT_LE(0, close(unnamed_sockets[1])); |
| |
| /* Reads from pathname sockets. */ |
| data_socket = accept(stream_pathname_socket, NULL, NULL); |
| ASSERT_LE(0, data_socket); |
| ASSERT_EQ(1, read(data_socket, &buf_parent, sizeof(buf_parent))); |
| ASSERT_EQ('b', buf_parent); |
| EXPECT_EQ(0, close(data_socket)); |
| EXPECT_EQ(0, close(stream_pathname_socket)); |
| |
| ASSERT_EQ(1, |
| read(dgram_pathname_socket, &buf_parent, sizeof(buf_parent))); |
| ASSERT_EQ('c', buf_parent); |
| ASSERT_EQ(1, |
| read(dgram_pathname_socket, &buf_parent, sizeof(buf_parent))); |
| ASSERT_EQ('d', buf_parent); |
| EXPECT_EQ(0, close(dgram_pathname_socket)); |
| |
| if (variant->domain != SCOPE_SANDBOX) { |
| /* Reads from abstract sockets if allowed to send. */ |
| data_socket = accept(stream_abstract_socket, NULL, NULL); |
| ASSERT_LE(0, data_socket); |
| ASSERT_EQ(1, |
| read(data_socket, &buf_parent, sizeof(buf_parent))); |
| ASSERT_EQ('e', buf_parent); |
| EXPECT_EQ(0, close(data_socket)); |
| |
| ASSERT_EQ(1, read(dgram_abstract_socket, &buf_parent, |
| sizeof(buf_parent))); |
| ASSERT_EQ('f', buf_parent); |
| ASSERT_EQ(1, read(dgram_abstract_socket, &buf_parent, |
| sizeof(buf_parent))); |
| ASSERT_EQ('g', buf_parent); |
| } |
| |
| /* Waits for all abstract socket tests. */ |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| EXPECT_EQ(0, close(stream_abstract_socket)); |
| EXPECT_EQ(0, close(dgram_abstract_socket)); |
| |
| if (WIFSIGNALED(status) || !WIFEXITED(status) || |
| WEXITSTATUS(status) != EXIT_SUCCESS) |
| _metadata->exit_code = KSFT_FAIL; |
| } |
| |
| TEST(datagram_sockets) |
| { |
| struct service_fixture connected_addr, non_connected_addr; |
| int server_conn_socket, server_unconn_socket; |
| int pipe_parent[2], pipe_child[2]; |
| int status; |
| char buf; |
| pid_t child; |
| |
| drop_caps(_metadata); |
| memset(&connected_addr, 0, sizeof(connected_addr)); |
| set_unix_address(&connected_addr, 0); |
| memset(&non_connected_addr, 0, sizeof(non_connected_addr)); |
| set_unix_address(&non_connected_addr, 1); |
| |
| ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); |
| ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); |
| |
| child = fork(); |
| ASSERT_LE(0, child); |
| if (child == 0) { |
| int client_conn_socket, client_unconn_socket; |
| |
| EXPECT_EQ(0, close(pipe_parent[1])); |
| EXPECT_EQ(0, close(pipe_child[0])); |
| |
| client_conn_socket = socket(AF_UNIX, SOCK_DGRAM, 0); |
| client_unconn_socket = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, client_conn_socket); |
| ASSERT_LE(0, client_unconn_socket); |
| |
| /* Waits for parent to listen. */ |
| ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); |
| ASSERT_EQ(0, |
| connect(client_conn_socket, &connected_addr.unix_addr, |
| connected_addr.unix_addr_len)); |
| |
| /* |
| * Both connected and non-connected sockets can send data when |
| * the domain is not scoped. |
| */ |
| ASSERT_EQ(1, send(client_conn_socket, ".", 1, 0)); |
| ASSERT_EQ(1, sendto(client_unconn_socket, ".", 1, 0, |
| &non_connected_addr.unix_addr, |
| non_connected_addr.unix_addr_len)); |
| ASSERT_EQ(1, write(pipe_child[1], ".", 1)); |
| |
| /* Scopes the domain. */ |
| create_scoped_domain(_metadata, |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| /* |
| * Connected socket sends data to the receiver, but the |
| * non-connected socket must fail to send data. |
| */ |
| ASSERT_EQ(1, send(client_conn_socket, ".", 1, 0)); |
| ASSERT_EQ(-1, sendto(client_unconn_socket, ".", 1, 0, |
| &non_connected_addr.unix_addr, |
| non_connected_addr.unix_addr_len)); |
| ASSERT_EQ(EPERM, errno); |
| ASSERT_EQ(1, write(pipe_child[1], ".", 1)); |
| |
| EXPECT_EQ(0, close(client_conn_socket)); |
| EXPECT_EQ(0, close(client_unconn_socket)); |
| _exit(_metadata->exit_code); |
| return; |
| } |
| EXPECT_EQ(0, close(pipe_parent[0])); |
| EXPECT_EQ(0, close(pipe_child[1])); |
| |
| server_conn_socket = socket(AF_UNIX, SOCK_DGRAM, 0); |
| server_unconn_socket = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, server_conn_socket); |
| ASSERT_LE(0, server_unconn_socket); |
| |
| ASSERT_EQ(0, bind(server_conn_socket, &connected_addr.unix_addr, |
| connected_addr.unix_addr_len)); |
| ASSERT_EQ(0, bind(server_unconn_socket, &non_connected_addr.unix_addr, |
| non_connected_addr.unix_addr_len)); |
| ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); |
| |
| /* Waits for child to test. */ |
| ASSERT_EQ(1, read(pipe_child[0], &buf, 1)); |
| ASSERT_EQ(1, recv(server_conn_socket, &buf, 1, 0)); |
| ASSERT_EQ(1, recv(server_unconn_socket, &buf, 1, 0)); |
| |
| /* |
| * Connected datagram socket will receive data, but |
| * non-connected datagram socket does not receive data. |
| */ |
| ASSERT_EQ(1, read(pipe_child[0], &buf, 1)); |
| ASSERT_EQ(1, recv(server_conn_socket, &buf, 1, 0)); |
| |
| /* Waits for all tests to finish. */ |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| EXPECT_EQ(0, close(server_conn_socket)); |
| EXPECT_EQ(0, close(server_unconn_socket)); |
| |
| if (WIFSIGNALED(status) || !WIFEXITED(status) || |
| WEXITSTATUS(status) != EXIT_SUCCESS) |
| _metadata->exit_code = KSFT_FAIL; |
| } |
| |
| TEST(self_connect) |
| { |
| struct service_fixture connected_addr, non_connected_addr; |
| int connected_socket, non_connected_socket, status; |
| pid_t child; |
| |
| drop_caps(_metadata); |
| memset(&connected_addr, 0, sizeof(connected_addr)); |
| set_unix_address(&connected_addr, 0); |
| memset(&non_connected_addr, 0, sizeof(non_connected_addr)); |
| set_unix_address(&non_connected_addr, 1); |
| |
| connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0); |
| non_connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0); |
| ASSERT_LE(0, connected_socket); |
| ASSERT_LE(0, non_connected_socket); |
| |
| ASSERT_EQ(0, bind(connected_socket, &connected_addr.unix_addr, |
| connected_addr.unix_addr_len)); |
| ASSERT_EQ(0, bind(non_connected_socket, &non_connected_addr.unix_addr, |
| non_connected_addr.unix_addr_len)); |
| |
| child = fork(); |
| ASSERT_LE(0, child); |
| if (child == 0) { |
| /* Child's domain is scoped. */ |
| create_scoped_domain(_metadata, |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| /* |
| * The child inherits the sockets, and cannot connect or |
| * send data to them. |
| */ |
| ASSERT_EQ(-1, |
| connect(connected_socket, &connected_addr.unix_addr, |
| connected_addr.unix_addr_len)); |
| ASSERT_EQ(EPERM, errno); |
| |
| ASSERT_EQ(-1, sendto(connected_socket, ".", 1, 0, |
| &connected_addr.unix_addr, |
| connected_addr.unix_addr_len)); |
| ASSERT_EQ(EPERM, errno); |
| |
| ASSERT_EQ(-1, sendto(non_connected_socket, ".", 1, 0, |
| &non_connected_addr.unix_addr, |
| non_connected_addr.unix_addr_len)); |
| ASSERT_EQ(EPERM, errno); |
| |
| EXPECT_EQ(0, close(connected_socket)); |
| EXPECT_EQ(0, close(non_connected_socket)); |
| _exit(_metadata->exit_code); |
| return; |
| } |
| |
| /* Waits for all tests to finish. */ |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| EXPECT_EQ(0, close(connected_socket)); |
| EXPECT_EQ(0, close(non_connected_socket)); |
| |
| if (WIFSIGNALED(status) || !WIFEXITED(status) || |
| WEXITSTATUS(status) != EXIT_SUCCESS) |
| _metadata->exit_code = KSFT_FAIL; |
| } |
| |
| /* Trace tests */ |
| |
| /* clang-format off */ |
| FIXTURE(trace_unix) { |
| /* clang-format on */ |
| int tracefs_ok; |
| }; |
| |
| FIXTURE_SETUP(trace_unix) |
| { |
| int ret; |
| |
| set_cap(_metadata, CAP_SYS_ADMIN); |
| ASSERT_EQ(0, unshare(CLONE_NEWNS | CLONE_NEWNET)); |
| 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_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE, |
| true)); |
| ASSERT_EQ(0, tracefs_clear()); |
| clear_cap(_metadata, CAP_SYS_ADMIN); |
| } |
| |
| FIXTURE_TEARDOWN(trace_unix) |
| { |
| if (!self->tracefs_ok) |
| return; |
| |
| set_cap(_metadata, CAP_SYS_ADMIN); |
| tracefs_enable_event(TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE, |
| false); |
| tracefs_fixture_teardown(); |
| clear_cap(_metadata, CAP_SYS_ADMIN); |
| } |
| |
| static const char |
| trace_unix_max_name[sizeof(((struct sockaddr_un *)0)->sun_path)] = { |
| [0 ... sizeof(trace_unix_max_name) - 2] = 'x', |
| }; |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT(trace_unix) { |
| /* clang-format on */ |
| int sock_type; /* SOCK_STREAM (connect) or SOCK_DGRAM (sendto). */ |
| bool sandbox; |
| bool sandbox_target; /* Peer owned by a domain: peer_domain != 0. */ |
| int expect_denied; |
| const char *name; /* NULL generates a PID-based binary name. */ |
| size_t name_len; |
| }; |
| |
| /* clang-format off */ |
| |
| /* Stream: sandboxed client connect() to an unsandboxed peer (peer_domain=0). */ |
| FIXTURE_VARIANT_ADD(trace_unix, stream_denied) { |
| .sock_type = SOCK_STREAM, .sandbox = true, |
| .sandbox_target = false, .expect_denied = 1, |
| }; |
| |
| /* Stream: peer socket owned by a domain, so peer_domain != 0. */ |
| FIXTURE_VARIANT_ADD(trace_unix, stream_denied_scoped_peer) { |
| .sock_type = SOCK_STREAM, .sandbox = true, |
| .sandbox_target = true, .expect_denied = 1, |
| }; |
| |
| /* Stream: unsandboxed client, connect() succeeds, no event. */ |
| FIXTURE_VARIANT_ADD(trace_unix, stream_allowed) { |
| .sock_type = SOCK_STREAM, .sandbox = false, |
| .sandbox_target = false, .expect_denied = 0, |
| }; |
| |
| /* Stream: lower abstract-name length boundary. */ |
| FIXTURE_VARIANT_ADD(trace_unix, stream_denied_empty_name) { |
| .sock_type = SOCK_STREAM, |
| .sandbox = true, |
| .sandbox_target = false, |
| .expect_denied = 1, |
| .name = "", |
| .name_len = 0, |
| }; |
| |
| /* Stream: upper abstract-name length boundary. */ |
| FIXTURE_VARIANT_ADD(trace_unix, stream_denied_max_name) { |
| .sock_type = SOCK_STREAM, |
| .sandbox = true, |
| .sandbox_target = false, |
| .expect_denied = 1, |
| .name = trace_unix_max_name, |
| .name_len = sizeof(trace_unix_max_name) - 1, |
| }; |
| |
| /* Datagram: sandboxed client sendto() an unsandboxed peer (peer_domain=0). */ |
| FIXTURE_VARIANT_ADD(trace_unix, dgram_denied) { |
| .sock_type = SOCK_DGRAM, .sandbox = true, |
| .sandbox_target = false, .expect_denied = 1, |
| }; |
| |
| /* Datagram: peer socket owned by a domain, so peer_domain != 0. */ |
| FIXTURE_VARIANT_ADD(trace_unix, dgram_denied_scoped_peer) { |
| .sock_type = SOCK_DGRAM, .sandbox = true, |
| .sandbox_target = true, .expect_denied = 1, |
| }; |
| |
| /* Datagram: unsandboxed client, sendto() succeeds, no event. */ |
| FIXTURE_VARIANT_ADD(trace_unix, dgram_allowed) { |
| .sock_type = SOCK_DGRAM, .sandbox = false, |
| .sandbox_target = false, .expect_denied = 0, |
| }; |
| |
| /* clang-format on */ |
| |
| /* |
| * A sandboxed thread reaching an abstract unix socket peer through connect(2) |
| * (stream) or sendto(2) (datagram) is denied and emits |
| * landlock_deny_scope_abstract_unix_socket. The default abstract name has a |
| * space and an embedded NUL followed by an "END" marker to check escaping and |
| * binary length handling. Additional stream variants cover the minimum and |
| * maximum abstract-name lengths. peer_pid is only meaningful for a stream peer |
| * (a datagram peer has no SO_PEERCRED), so it is asserted only there. |
| */ |
| TEST_F(trace_unix, deny_scope_unix) |
| { |
| struct sockaddr_un addr = { |
| .sun_family = AF_UNIX, |
| }; |
| char *buf, field[128], expected_pid[16]; |
| int server_fd, count, status, name_len, addr_len; |
| pid_t child; |
| |
| if (!self->tracefs_ok) |
| SKIP(return, "tracefs not available"); |
| |
| /* |
| * For the non-zero peer_domain case, sandbox the parent before it |
| * creates the server socket, so the socket carries the parent's domain |
| * and peer_domain= is non-zero. |
| */ |
| if (variant->sandbox_target) |
| create_scoped_domain(_metadata, |
| LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); |
| |
| server_fd = socket(AF_UNIX, variant->sock_type | SOCK_CLOEXEC, 0); |
| ASSERT_LE(0, server_fd); |
| |
| addr.sun_path[0] = '\0'; |
| if (variant->name) { |
| ASSERT_LE(variant->name_len, sizeof(addr.sun_path) - 1); |
| memcpy(addr.sun_path + 1, variant->name, variant->name_len); |
| name_len = variant->name_len; |
| } else { |
| name_len = snprintf(addr.sun_path + 1, |
| sizeof(addr.sun_path) - 1, |
| "landlock_trace_test_%d ", getpid()); |
| addr.sun_path[1 + name_len] = '\0'; |
| memcpy(addr.sun_path + 1 + name_len + 1, "END", 3); |
| name_len += 1 + 3; |
| } |
| addr_len = offsetof(struct sockaddr_un, sun_path) + 1 + name_len; |
| |
| ASSERT_EQ(0, bind(server_fd, (struct sockaddr *)&addr, addr_len)); |
| if (variant->sock_type == SOCK_STREAM) |
| ASSERT_EQ(0, listen(server_fd, 1)); |
| |
| child = fork(); |
| ASSERT_LE(0, child); |
| |
| if (child == 0) { |
| int client_fd, ret; |
| |
| if (variant->sandbox) { |
| struct landlock_ruleset_attr ruleset_attr = { |
| .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, |
| }; |
| int ruleset_fd; |
| |
| 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); |
| } |
| |
| client_fd = |
| socket(AF_UNIX, variant->sock_type | SOCK_CLOEXEC, 0); |
| if (client_fd < 0) |
| _exit(1); |
| |
| if (variant->sock_type == SOCK_STREAM) |
| ret = connect(client_fd, (struct sockaddr *)&addr, |
| addr_len); |
| else |
| ret = sendto(client_fd, ".", 1, 0, |
| (struct sockaddr *)&addr, addr_len); |
| |
| if (variant->sandbox) { |
| /* Reaching the peer should be denied. */ |
| if (ret != -1 || errno != EPERM) { |
| close(client_fd); |
| _exit(2); |
| } |
| } else { |
| /* No sandbox: stream connect() == 0, sendto() == 1. */ |
| int ok = variant->sock_type == SOCK_STREAM ? 0 : 1; |
| |
| if (ret != ok) { |
| close(client_fd); |
| _exit(2); |
| } |
| } |
| close(client_fd); |
| _exit(0); |
| } |
| |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| ASSERT_TRUE(WIFEXITED(status)); |
| EXPECT_EQ(0, WEXITSTATUS(status)); |
| close(server_fd); |
| |
| buf = tracefs_read_buf(); |
| ASSERT_NE(NULL, buf); |
| |
| count = tracefs_count_matches( |
| buf, REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(TRACE_TASK)); |
| if (!variant->expect_denied) { |
| EXPECT_EQ(0, count) |
| { |
| TH_LOG("Expected 0 deny_scope events, got %d\n%s", |
| count, buf); |
| } |
| free(buf); |
| return; |
| } |
| |
| EXPECT_EQ(variant->expect_denied, count) |
| { |
| TH_LOG("Expected deny_scope_abstract_unix_socket event, " |
| "got %d\n%s", |
| count, buf); |
| } |
| |
| ASSERT_EQ(0, tracefs_extract_field( |
| buf, |
| REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(TRACE_TASK), |
| "sun_path", field, sizeof(field))); |
| if (variant->name) { |
| EXPECT_STREQ(variant->name, field); |
| } else { |
| /* An embedded NUL must not truncate the following marker. */ |
| EXPECT_NE(NULL, strstr(field, "END")) |
| { |
| TH_LOG("sun_path truncated or unescaped: %s", field); |
| } |
| } |
| |
| /* peer_pid is the parent's PID for a stream peer (0 for datagram). */ |
| if (variant->sock_type == SOCK_STREAM) { |
| snprintf(expected_pid, sizeof(expected_pid), "%d", getpid()); |
| ASSERT_EQ(0, tracefs_extract_field( |
| buf, |
| REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET( |
| TRACE_TASK), |
| "peer_pid", field, sizeof(field))); |
| EXPECT_STREQ(expected_pid, field); |
| } |
| |
| /* peer_domain: 0 when the peer is unsandboxed, non-zero otherwise. */ |
| ASSERT_EQ(0, tracefs_extract_field( |
| buf, |
| REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(TRACE_TASK), |
| "peer_domain", field, sizeof(field))); |
| EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0) |
| { |
| TH_LOG("Unexpected peer_domain=%s", field); |
| } |
| |
| free(buf); |
| } |
| |
| TEST_HARNESS_MAIN |