mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-27 19:51:03 +00:00
CPU-painting mode skips Skia GPU backend initialization in the Compositor. On macOS the old WebGL path still worked because that initialization happened before the sandbox was applied, opening Metal's AGX device user client while the process was still unsandboxed. The CPU-backed WebGL path still uses ANGLE's Metal backend for GL command execution, so ANGLE now performs the first Metal device open after the Compositor sandbox is active. The sandbox then denies AGXDeviceUserClient and WebGL context creation fails. Add an optional IOKit user-client allowance list to the macOS sandbox helper, and opt only the Compositor into AGXDeviceUserClient. This keeps the default helper sandbox unchanged while allowing ANGLE to initialize Metal when WebGL is used in CPU-painting mode.
540 lines
16 KiB
C++
540 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 ErrorOr<void> append_allowed_iokit_user_client_classes(StringBuilder& builder, ReadonlySpan<StringView> iokit_user_client_classes)
|
|
{
|
|
for (auto const& user_client_class : iokit_user_client_classes) {
|
|
builder.append("(allow iokit-open-user-client (iokit-user-client-class "sv);
|
|
append_sandbox_string_literal(builder, user_client_class);
|
|
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, ReadonlySpan<StringView> iokit_user_client_classes)
|
|
{
|
|
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));
|
|
TRY(append_allowed_iokit_user_client_classes(profile, iokit_user_client_classes));
|
|
|
|
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
|
|
|
|
}
|