Include directory read access in Landlock read-write directory rules. Without it, paths such as /dev/dri could be writable but not enumerable after sandboxing, which made Mesa fail while probing GPU devices. Allow the Compositor to open /dev/udmabuf as well when the device is present, matching the other GPU device nodes exposed to the process.
528 lines
16 KiB
C++
528 lines
16 KiB
C++
/*
|
|
* Copyright (c) 2026-present, the Ladybird developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibSandbox/Sandbox.h>
|
|
|
|
#if defined(AK_OS_LINUX)
|
|
# include <AK/LexicalPath.h>
|
|
# include <AK/ScopeGuard.h>
|
|
# include <AK/Vector.h>
|
|
# include <errno.h>
|
|
# include <fcntl.h>
|
|
# include <linux/landlock.h>
|
|
# include <sys/prctl.h>
|
|
# include <sys/stat.h>
|
|
# include <sys/syscall.h>
|
|
# include <unistd.h>
|
|
#endif
|
|
|
|
#if defined(AK_OS_MACOS)
|
|
# include <AK/LexicalPath.h>
|
|
# include <AK/StringBuilder.h>
|
|
# include <errno.h>
|
|
# include <limits.h>
|
|
# include <sandbox.h>
|
|
# include <signal.h>
|
|
# include <stdlib.h>
|
|
# include <sys/stat.h>
|
|
# include <unistd.h>
|
|
|
|
extern "C" {
|
|
int sandbox_init_with_parameters(char const* profile, u64 flags, char const* const parameters[], char** errorbuf);
|
|
}
|
|
#endif
|
|
|
|
#if defined(__GLIBC__)
|
|
# include <malloc.h>
|
|
#endif
|
|
|
|
namespace Sandbox {
|
|
|
|
ErrorOr<void> install_no_new_privileges()
|
|
{
|
|
#if defined(AK_OS_LINUX)
|
|
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0)
|
|
return Error::from_syscall("prctl(PR_SET_NO_NEW_PRIVS)"sv, errno);
|
|
#endif
|
|
return {};
|
|
}
|
|
|
|
ErrorOr<void> configure_runtime()
|
|
{
|
|
#if defined(AK_OS_LINUX) && defined(__GLIBC__) && defined(M_ARENA_MAX) && !defined(HAS_ADDRESS_SANITIZER)
|
|
if (mallopt(M_ARENA_MAX, 4) == 0)
|
|
return Error::from_string_literal("mallopt(M_ARENA_MAX) failed");
|
|
#endif
|
|
return {};
|
|
}
|
|
|
|
#if defined(AK_OS_LINUX)
|
|
ErrorOr<void> add_landlock_path_if_exists(Vector<LandlockPath>& paths, StringView path, LandlockPath::Access access)
|
|
{
|
|
auto path_bytes = path.to_byte_string();
|
|
|
|
struct stat statbuf;
|
|
if (stat(path_bytes.characters(), &statbuf) < 0) {
|
|
if (errno == ENOENT)
|
|
return {};
|
|
return Error::from_syscall("stat"sv, errno);
|
|
}
|
|
|
|
bool is_directory = S_ISDIR(statbuf.st_mode);
|
|
|
|
for (auto const& existing_path : paths) {
|
|
if (existing_path.access == access && existing_path.path == path_bytes)
|
|
return {};
|
|
}
|
|
|
|
TRY(paths.try_append({ move(path_bytes), access, is_directory }));
|
|
return {};
|
|
}
|
|
#endif
|
|
|
|
#if defined(AK_OS_MACOS)
|
|
ErrorOr<void> add_seatbelt_path_if_exists(Vector<SeatbeltPath>& paths, StringView path, SeatbeltPath::Access access)
|
|
{
|
|
auto path_bytes = path.to_byte_string();
|
|
|
|
struct stat statbuf;
|
|
if (stat(path_bytes.characters(), &statbuf) < 0) {
|
|
if (errno == ENOENT)
|
|
return {};
|
|
return Error::from_syscall("stat"sv, errno);
|
|
}
|
|
|
|
char resolved_path[PATH_MAX];
|
|
if (realpath(path_bytes.characters(), resolved_path) == nullptr)
|
|
return Error::from_syscall("realpath"sv, errno);
|
|
path_bytes = resolved_path;
|
|
|
|
auto is_directory = S_ISDIR(statbuf.st_mode);
|
|
|
|
for (auto const& existing_path : paths) {
|
|
if (existing_path.access == access && existing_path.path == path_bytes)
|
|
return {};
|
|
}
|
|
|
|
TRY(paths.try_append({ move(path_bytes), access, is_directory }));
|
|
return {};
|
|
}
|
|
|
|
static void append_sandbox_string_literal(StringBuilder& builder, StringView string)
|
|
{
|
|
builder.append('"');
|
|
for (auto ch : string.bytes()) {
|
|
if (ch == '"' || ch == '\\')
|
|
builder.append('\\');
|
|
builder.append(static_cast<char>(ch));
|
|
}
|
|
builder.append('"');
|
|
}
|
|
|
|
static void append_sandbox_path_filter(StringBuilder& builder, SeatbeltPath const& path)
|
|
{
|
|
builder.append(path.is_directory ? "(subpath "sv : "(literal "sv);
|
|
append_sandbox_string_literal(builder, path.path);
|
|
builder.append(')');
|
|
}
|
|
|
|
static bool seatbelt_path_allows_access(SeatbeltPath::Access path_access, SeatbeltPath::Access requested_access)
|
|
{
|
|
if (requested_access == SeatbeltPath::Access::ReadOnly)
|
|
return true;
|
|
return path_access == requested_access;
|
|
}
|
|
|
|
static ErrorOr<void> append_allowed_paths(StringBuilder& builder, StringView operation, ReadonlySpan<SeatbeltPath> paths, SeatbeltPath::Access access)
|
|
{
|
|
bool emitted_header = false;
|
|
for (auto const& path : paths) {
|
|
if (!seatbelt_path_allows_access(path.access, access))
|
|
continue;
|
|
|
|
if (!emitted_header) {
|
|
builder.append("(allow "sv);
|
|
builder.append(operation);
|
|
emitted_header = true;
|
|
}
|
|
builder.append(' ');
|
|
append_sandbox_path_filter(builder, path);
|
|
}
|
|
|
|
if (emitted_header)
|
|
builder.append(")\n"sv);
|
|
|
|
return {};
|
|
}
|
|
|
|
static ErrorOr<void> append_allowed_path_extensions(StringBuilder& builder, ReadonlySpan<SeatbeltPath> paths, SeatbeltPath::Access access)
|
|
{
|
|
auto extension_class = access == SeatbeltPath::Access::ReadWrite ? "com.apple.app-sandbox.read-write"sv : "com.apple.app-sandbox.read"sv;
|
|
|
|
bool emitted_header = false;
|
|
for (auto const& path : paths) {
|
|
if (!seatbelt_path_allows_access(path.access, access))
|
|
continue;
|
|
|
|
if (!emitted_header) {
|
|
builder.append("(allow file-issue-extension"sv);
|
|
emitted_header = true;
|
|
}
|
|
builder.append(" (require-all (extension-class "sv);
|
|
append_sandbox_string_literal(builder, extension_class);
|
|
builder.append(") "sv);
|
|
append_sandbox_path_filter(builder, path);
|
|
builder.append(')');
|
|
}
|
|
|
|
if (emitted_header)
|
|
builder.append(")\n"sv);
|
|
|
|
return {};
|
|
}
|
|
|
|
static ErrorOr<void> append_allowed_executables(StringBuilder& builder, ReadonlySpan<ByteString> executable_paths)
|
|
{
|
|
if (executable_paths.is_empty())
|
|
return {};
|
|
|
|
builder.append("(allow process-exec"sv);
|
|
for (auto const& path : executable_paths) {
|
|
builder.append(" (literal "sv);
|
|
append_sandbox_string_literal(builder, path);
|
|
builder.append(')');
|
|
}
|
|
builder.append(")\n"sv);
|
|
|
|
return {};
|
|
}
|
|
|
|
static void sandbox_violation_signal_handler(int)
|
|
{
|
|
char const message[] = "Sandbox violation: terminating process\n";
|
|
[[maybe_unused]] auto nwritten = write(STDERR_FILENO, message, sizeof(message) - 1);
|
|
_exit(128 + SIGSYS);
|
|
}
|
|
|
|
static ErrorOr<void> install_sandbox_violation_signal_handler()
|
|
{
|
|
struct sigaction action {};
|
|
action.sa_handler = sandbox_violation_signal_handler;
|
|
sigemptyset(&action.sa_mask);
|
|
action.sa_flags = SA_RESETHAND;
|
|
if (sigaction(SIGSYS, &action, nullptr) < 0)
|
|
return Error::from_syscall("sigaction(SIGSYS)"sv, errno);
|
|
return {};
|
|
}
|
|
|
|
ErrorOr<void> apply_macos_sandbox(ReadonlySpan<SeatbeltPath> paths, NetworkAccess network_access, ReadonlySpan<ByteString> executable_paths)
|
|
{
|
|
TRY(install_sandbox_violation_signal_handler());
|
|
|
|
StringBuilder profile;
|
|
TRY(profile.try_append(R"~~~(
|
|
(version 1)
|
|
(deny default
|
|
(with send-signal SIGSYS)
|
|
(with message "Ladybird macOS sandbox default deny"))
|
|
|
|
(allow process-info*)
|
|
(allow signal (target self))
|
|
(allow sysctl-read)
|
|
(allow system*)
|
|
(allow ipc*)
|
|
(allow mach*)
|
|
(allow iokit-open-user-client
|
|
(iokit-user-client-class "IOSurfaceRootUserClient"))
|
|
(allow user-preference-read
|
|
(preference-domain "kCFPreferencesAnyApplication")
|
|
(preference-domain "org.ladybird.ladybird"))
|
|
|
|
(allow network-outbound
|
|
(literal "/private/var/run/syslog"))
|
|
|
|
(deny syscall-unix
|
|
(with send-signal SIGKILL)
|
|
(with message "Ladybird macOS sandbox syscall deny"))
|
|
|
|
(allow syscall-unix
|
|
(syscall-group-bsdthread)
|
|
(syscall-group-close)
|
|
(syscall-group-fcntl)
|
|
(syscall-group-getfsstat)
|
|
(syscall-group-kevent)
|
|
(syscall-group-kqueue)
|
|
(syscall-group-mkdir)
|
|
(syscall-group-open)
|
|
(syscall-group-open-dprotected)
|
|
(syscall-group-pthread)
|
|
(syscall-group-pthread-cv)
|
|
(syscall-group-pthread-locks)
|
|
(syscall-group-read)
|
|
(syscall-group-recv)
|
|
(syscall-group-rlimit)
|
|
(syscall-group-select)
|
|
(syscall-group-send)
|
|
(syscall-group-signal)
|
|
(syscall-group-sockopt)
|
|
(syscall-group-stat)
|
|
(syscall-group-statfs)
|
|
(syscall-group-ulock)
|
|
(syscall-group-write)
|
|
(syscall-number
|
|
SYS___disable_threadsignal
|
|
SYS___channel_open
|
|
SYS___mac_syscall
|
|
SYS___semwait_signal
|
|
SYS___semwait_signal_nocancel
|
|
SYS_abort_with_payload
|
|
SYS_access
|
|
SYS_change_fdguard_np
|
|
SYS_connect
|
|
SYS_crossarch_trap
|
|
SYS_csops_audittoken
|
|
SYS_csrctl
|
|
SYS_dup
|
|
SYS_exit
|
|
SYS_faccessat
|
|
SYS_fileport_makefd
|
|
SYS_fileport_makeport
|
|
SYS_fgetattrlist
|
|
SYS_fgetxattr
|
|
SYS_flock
|
|
SYS_fsgetpath
|
|
SYS_fsync
|
|
SYS_ftruncate
|
|
SYS_getaudit_addr
|
|
SYS_getattrlist
|
|
SYS_getattrlistbulk
|
|
SYS_getdirentries64
|
|
SYS_getentropy
|
|
SYS_getegid
|
|
SYS_geteuid
|
|
SYS_getgid
|
|
SYS_gethostuuid
|
|
SYS_getpeername
|
|
SYS_getpid
|
|
SYS_getrusage
|
|
SYS_getsockname
|
|
SYS_gettid
|
|
SYS_gettimeofday
|
|
SYS_getuid
|
|
SYS_getxattr
|
|
SYS_ioctl
|
|
SYS_issetugid
|
|
SYS_kdebug_trace
|
|
SYS_kdebug_trace64
|
|
SYS_kdebug_trace_string
|
|
SYS_kdebug_typefilter
|
|
SYS_listxattr
|
|
SYS_lseek
|
|
SYS_madvise
|
|
SYS_mlock
|
|
SYS_mmap
|
|
SYS_mprotect
|
|
SYS_mremap_encrypted
|
|
SYS_msync
|
|
SYS_munlock
|
|
SYS_munmap
|
|
SYS_necp_client_action
|
|
SYS_necp_open
|
|
SYS_open
|
|
SYS_open_nocancel
|
|
SYS_openat
|
|
SYS_os_fault_with_payload
|
|
SYS_pathconf
|
|
SYS_pipe
|
|
SYS_poll
|
|
SYS_posix_spawn
|
|
SYS_proc_info
|
|
SYS_readlink
|
|
SYS_rename
|
|
SYS_rmdir
|
|
SYS_sendfile
|
|
SYS_shm_open
|
|
SYS_shared_region_check_np
|
|
SYS_shared_region_map_and_slide_2_np
|
|
SYS_socket
|
|
SYS_socketpair
|
|
SYS_sysctl
|
|
SYS_sysctlbyname
|
|
SYS_thread_selfid
|
|
SYS_umask
|
|
SYS_wait4
|
|
SYS_work_interval_ctl
|
|
SYS_workq_kernreturn
|
|
SYS_workq_open))
|
|
|
|
(allow file-read-metadata)
|
|
(allow file-read*
|
|
(literal "/")
|
|
(literal "/dev/dtracehelper")
|
|
(literal "/dev/null")
|
|
(literal "/dev/random")
|
|
(literal "/dev/urandom")
|
|
(literal "/private/etc/localtime")
|
|
(subpath "/private/etc/ssl")
|
|
(subpath "/System")
|
|
(subpath "/Library/Preferences/Logging")
|
|
(subpath "/private/var/db/timezone")
|
|
(subpath "/usr/lib")
|
|
(subpath "/usr/share"))
|
|
|
|
(allow file-map-executable
|
|
(subpath "/System")
|
|
(subpath "/usr/lib"))
|
|
|
|
(allow file-write-data file-ioctl
|
|
(literal "/dev/dtracehelper"))
|
|
)~~~"sv));
|
|
|
|
if (network_access == NetworkAccess::Allowed)
|
|
TRY(profile.try_append("(allow network*)\n"sv));
|
|
|
|
TRY(append_allowed_paths(profile, "file-read*"sv, paths, SeatbeltPath::Access::ReadOnly));
|
|
TRY(append_allowed_paths(profile, "file-map-executable"sv, paths, SeatbeltPath::Access::ReadAndExecute));
|
|
TRY(append_allowed_paths(profile, "file-write*"sv, paths, SeatbeltPath::Access::ReadWrite));
|
|
TRY(append_allowed_path_extensions(profile, paths, SeatbeltPath::Access::ReadOnly));
|
|
TRY(append_allowed_path_extensions(profile, paths, SeatbeltPath::Access::ReadWrite));
|
|
TRY(append_allowed_executables(profile, executable_paths));
|
|
|
|
auto profile_string = profile.to_byte_string();
|
|
|
|
char* errorbuf = nullptr;
|
|
# pragma clang diagnostic push
|
|
# pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
auto result = sandbox_init_with_parameters(profile_string.characters(), 0, nullptr, &errorbuf);
|
|
# pragma clang diagnostic pop
|
|
if (result < 0) {
|
|
if (errorbuf) {
|
|
# pragma clang diagnostic push
|
|
# pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
sandbox_free_error(errorbuf);
|
|
# pragma clang diagnostic pop
|
|
}
|
|
return Error::from_string_literal("sandbox_init_with_parameters failed");
|
|
}
|
|
return {};
|
|
}
|
|
#endif
|
|
|
|
#if defined(AK_OS_LINUX)
|
|
ErrorOr<void> restrict_filesystem_with_landlock(ReadonlySpan<LandlockPath> paths)
|
|
{
|
|
# if defined(__NR_landlock_create_ruleset) && defined(__NR_landlock_add_rule) && defined(__NR_landlock_restrict_self)
|
|
auto landlock_abi = syscall(__NR_landlock_create_ruleset, nullptr, 0, LANDLOCK_CREATE_RULESET_VERSION);
|
|
if (landlock_abi < 0) {
|
|
if (errno == ENOSYS || errno == EOPNOTSUPP || errno == EINVAL)
|
|
return {};
|
|
return Error::from_syscall("landlock_create_ruleset(LANDLOCK_CREATE_RULESET_VERSION)"sv, errno);
|
|
}
|
|
if (landlock_abi == 0)
|
|
return {};
|
|
|
|
landlock_ruleset_attr ruleset_attributes {};
|
|
ruleset_attributes.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE
|
|
| LANDLOCK_ACCESS_FS_WRITE_FILE
|
|
| LANDLOCK_ACCESS_FS_READ_FILE
|
|
| LANDLOCK_ACCESS_FS_READ_DIR
|
|
| LANDLOCK_ACCESS_FS_REMOVE_DIR
|
|
| LANDLOCK_ACCESS_FS_REMOVE_FILE
|
|
| LANDLOCK_ACCESS_FS_MAKE_CHAR
|
|
| LANDLOCK_ACCESS_FS_MAKE_DIR
|
|
| LANDLOCK_ACCESS_FS_MAKE_REG
|
|
| LANDLOCK_ACCESS_FS_MAKE_SOCK
|
|
| LANDLOCK_ACCESS_FS_MAKE_FIFO
|
|
| LANDLOCK_ACCESS_FS_MAKE_BLOCK
|
|
| LANDLOCK_ACCESS_FS_MAKE_SYM;
|
|
|
|
# ifdef LANDLOCK_ACCESS_FS_REFER
|
|
if (landlock_abi >= 2)
|
|
ruleset_attributes.handled_access_fs |= LANDLOCK_ACCESS_FS_REFER;
|
|
# endif
|
|
# ifdef LANDLOCK_ACCESS_FS_TRUNCATE
|
|
if (landlock_abi >= 3)
|
|
ruleset_attributes.handled_access_fs |= LANDLOCK_ACCESS_FS_TRUNCATE;
|
|
# endif
|
|
# if defined(LANDLOCK_ACCESS_NET_BIND_TCP) && defined(LANDLOCK_ACCESS_NET_CONNECT_TCP)
|
|
auto ruleset_attributes_size = offsetof(landlock_ruleset_attr, handled_access_net);
|
|
# else
|
|
auto ruleset_attributes_size = sizeof(ruleset_attributes);
|
|
# endif
|
|
auto ruleset_fd = syscall(__NR_landlock_create_ruleset, &ruleset_attributes, ruleset_attributes_size, 0);
|
|
if (ruleset_fd < 0)
|
|
return Error::from_syscall("landlock_create_ruleset"sv, errno);
|
|
|
|
ArmedScopeGuard close_ruleset_fd = [&] {
|
|
close(static_cast<int>(ruleset_fd));
|
|
};
|
|
|
|
for (auto const& landlock_path : paths) {
|
|
auto path_fd = open(landlock_path.path.characters(), O_PATH | O_CLOEXEC);
|
|
if (path_fd < 0)
|
|
return Error::from_syscall("open(O_PATH)"sv, errno);
|
|
|
|
ArmedScopeGuard close_path_fd = [&] {
|
|
close(path_fd);
|
|
};
|
|
|
|
landlock_path_beneath_attr path_beneath {};
|
|
switch (landlock_path.access) {
|
|
case LandlockPath::Access::ReadOnly: {
|
|
path_beneath.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE;
|
|
|
|
if (landlock_path.is_directory)
|
|
path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_READ_DIR;
|
|
break;
|
|
}
|
|
case LandlockPath::Access::ReadAndExecute: {
|
|
path_beneath.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_EXECUTE;
|
|
|
|
if (landlock_path.is_directory)
|
|
path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_READ_DIR;
|
|
break;
|
|
}
|
|
case LandlockPath::Access::ReadWrite: {
|
|
path_beneath.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE;
|
|
|
|
# ifdef LANDLOCK_ACCESS_FS_TRUNCATE
|
|
if (landlock_abi >= 3)
|
|
path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_TRUNCATE;
|
|
# endif
|
|
|
|
if (landlock_path.is_directory) {
|
|
path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_READ_DIR
|
|
| LANDLOCK_ACCESS_FS_REMOVE_DIR
|
|
| LANDLOCK_ACCESS_FS_REMOVE_FILE
|
|
| LANDLOCK_ACCESS_FS_MAKE_DIR
|
|
| LANDLOCK_ACCESS_FS_MAKE_REG
|
|
| LANDLOCK_ACCESS_FS_MAKE_SOCK
|
|
| LANDLOCK_ACCESS_FS_MAKE_FIFO;
|
|
|
|
# ifdef LANDLOCK_ACCESS_FS_REFER
|
|
if (landlock_abi >= 2)
|
|
path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_REFER;
|
|
# endif
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
path_beneath.parent_fd = path_fd;
|
|
if (syscall(__NR_landlock_add_rule, ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0) < 0)
|
|
return Error::from_syscall("landlock_add_rule"sv, errno);
|
|
}
|
|
|
|
if (syscall(__NR_landlock_restrict_self, ruleset_fd, 0) < 0)
|
|
return Error::from_syscall("landlock_restrict_self"sv, errno);
|
|
# else
|
|
(void)paths;
|
|
# endif
|
|
|
|
return {};
|
|
}
|
|
#endif
|
|
|
|
}
|