LibSandbox: Add macOS service sandboxing

Add Seatbelt-based macOS sandboxing for the browser service processes.
The shared profile builder grants only the filesystem, network, Mach,
and process execution permissions each service needs, with fatal sandbox
violation reporting enabled so denials are visible during development.

Wire sandbox profiles into WebContent, WebWorker, RequestServer,
ImageDecoder, and Compositor. Keep Landlock and Seatbelt APIs visible
only on the platforms that use them. Allow RequestServer resource
substitution files explicitly, preserve read access for read-write cache
paths, and only grant renderer process execution for an existing
Cranelift helper.
This commit is contained in:
Andreas Kling 2026-06-10 20:27:25 +02:00 committed by Andreas Kling
parent fc02ab95fe
commit ddbc3e2006
13 changed files with 635 additions and 19 deletions

View file

@ -19,6 +19,22 @@
# include <unistd.h> # include <unistd.h>
#endif #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__) #if defined(__GLIBC__)
# include <malloc.h> # include <malloc.h>
#endif #endif
@ -43,9 +59,9 @@ ErrorOr<void> configure_runtime()
return {}; return {};
} }
#if defined(AK_OS_LINUX)
ErrorOr<void> add_landlock_path_if_exists(Vector<LandlockPath>& paths, StringView path, LandlockPath::Access access) ErrorOr<void> add_landlock_path_if_exists(Vector<LandlockPath>& paths, StringView path, LandlockPath::Access access)
{ {
#if defined(AK_OS_LINUX)
auto path_bytes = path.to_byte_string(); auto path_bytes = path.to_byte_string();
struct stat statbuf; struct stat statbuf;
@ -64,17 +80,342 @@ ErrorOr<void> add_landlock_path_if_exists(Vector<LandlockPath>& paths, StringVie
} }
TRY(paths.try_append({ move(path_bytes), access })); TRY(paths.try_append({ move(path_bytes), access }));
#else return {};
(void)paths; }
(void)path;
(void)access;
#endif #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 {}; 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) ErrorOr<void> restrict_filesystem_with_landlock(ReadonlySpan<LandlockPath> paths)
{ {
#if defined(AK_OS_LINUX) && defined(__NR_landlock_create_ruleset) && defined(__NR_landlock_add_rule) && defined(__NR_landlock_restrict_self) # 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); auto landlock_abi = syscall(__NR_landlock_create_ruleset, nullptr, 0, LANDLOCK_CREATE_RULESET_VERSION);
if (landlock_abi < 0) { if (landlock_abi < 0) {
if (errno == ENOSYS || errno == EOPNOTSUPP || errno == EINVAL) if (errno == ENOSYS || errno == EOPNOTSUPP || errno == EINVAL)
@ -99,19 +440,19 @@ ErrorOr<void> restrict_filesystem_with_landlock(ReadonlySpan<LandlockPath> paths
| LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_BLOCK
| LANDLOCK_ACCESS_FS_MAKE_SYM; | LANDLOCK_ACCESS_FS_MAKE_SYM;
# ifdef LANDLOCK_ACCESS_FS_REFER # ifdef LANDLOCK_ACCESS_FS_REFER
if (landlock_abi >= 2) if (landlock_abi >= 2)
ruleset_attributes.handled_access_fs |= LANDLOCK_ACCESS_FS_REFER; ruleset_attributes.handled_access_fs |= LANDLOCK_ACCESS_FS_REFER;
# endif # endif
# ifdef LANDLOCK_ACCESS_FS_TRUNCATE # ifdef LANDLOCK_ACCESS_FS_TRUNCATE
if (landlock_abi >= 3) if (landlock_abi >= 3)
ruleset_attributes.handled_access_fs |= LANDLOCK_ACCESS_FS_TRUNCATE; ruleset_attributes.handled_access_fs |= LANDLOCK_ACCESS_FS_TRUNCATE;
# endif # endif
# if defined(LANDLOCK_ACCESS_NET_BIND_TCP) && defined(LANDLOCK_ACCESS_NET_CONNECT_TCP) # if defined(LANDLOCK_ACCESS_NET_BIND_TCP) && defined(LANDLOCK_ACCESS_NET_CONNECT_TCP)
auto ruleset_attributes_size = offsetof(landlock_ruleset_attr, handled_access_net); auto ruleset_attributes_size = offsetof(landlock_ruleset_attr, handled_access_net);
# else # else
auto ruleset_attributes_size = sizeof(ruleset_attributes); auto ruleset_attributes_size = sizeof(ruleset_attributes);
# endif # endif
auto ruleset_fd = syscall(__NR_landlock_create_ruleset, &ruleset_attributes, ruleset_attributes_size, 0); auto ruleset_fd = syscall(__NR_landlock_create_ruleset, &ruleset_attributes, ruleset_attributes_size, 0);
if (ruleset_fd < 0) if (ruleset_fd < 0)
return Error::from_syscall("landlock_create_ruleset"sv, errno); return Error::from_syscall("landlock_create_ruleset"sv, errno);
@ -141,14 +482,14 @@ ErrorOr<void> restrict_filesystem_with_landlock(ReadonlySpan<LandlockPath> paths
| LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_REG
| LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_SOCK
| LANDLOCK_ACCESS_FS_MAKE_FIFO; | LANDLOCK_ACCESS_FS_MAKE_FIFO;
# ifdef LANDLOCK_ACCESS_FS_REFER # ifdef LANDLOCK_ACCESS_FS_REFER
if (landlock_abi >= 2) if (landlock_abi >= 2)
path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_REFER; path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_REFER;
# endif # endif
# ifdef LANDLOCK_ACCESS_FS_TRUNCATE # ifdef LANDLOCK_ACCESS_FS_TRUNCATE
if (landlock_abi >= 3) if (landlock_abi >= 3)
path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_TRUNCATE; path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_TRUNCATE;
# endif # endif
} }
path_beneath.parent_fd = path_fd; path_beneath.parent_fd = path_fd;
if (syscall(__NR_landlock_add_rule, ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0) < 0) if (syscall(__NR_landlock_add_rule, ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0) < 0)
@ -157,9 +498,9 @@ ErrorOr<void> restrict_filesystem_with_landlock(ReadonlySpan<LandlockPath> paths
if (syscall(__NR_landlock_restrict_self, ruleset_fd, 0) < 0) if (syscall(__NR_landlock_restrict_self, ruleset_fd, 0) < 0)
return Error::from_syscall("landlock_restrict_self"sv, errno); return Error::from_syscall("landlock_restrict_self"sv, errno);
#else # else
(void)paths; (void)paths;
#endif # endif
return {}; return {};
} }
@ -171,5 +512,6 @@ ErrorOr<void> restrict_filesystem_with_landlock(ReadonlySpan<StringView> readabl
TRY(paths.try_append({ readable_path.to_byte_string(), LandlockPath::Access::ReadOnly })); TRY(paths.try_append({ readable_path.to_byte_string(), LandlockPath::Access::ReadOnly }));
return restrict_filesystem_with_landlock(paths.span()); return restrict_filesystem_with_landlock(paths.span());
} }
#endif
} }

View file

@ -8,12 +8,14 @@
#include <AK/ByteString.h> #include <AK/ByteString.h>
#include <AK/Error.h> #include <AK/Error.h>
#include <AK/Platform.h>
#include <AK/Span.h> #include <AK/Span.h>
#include <AK/StringView.h> #include <AK/StringView.h>
#include <AK/Vector.h> #include <AK/Vector.h>
namespace Sandbox { namespace Sandbox {
#if defined(AK_OS_LINUX)
struct LandlockPath { struct LandlockPath {
enum class Access { enum class Access {
ReadOnly, ReadOnly,
@ -24,11 +26,39 @@ struct LandlockPath {
ByteString path; ByteString path;
Access access { Access::ReadOnly }; Access access { Access::ReadOnly };
}; };
#endif
#if defined(AK_OS_MACOS)
struct SeatbeltPath {
enum class Access {
ReadOnly,
ReadAndExecute,
ReadWrite,
};
ByteString path;
Access access { Access::ReadOnly };
bool is_directory { false };
};
enum class NetworkAccess {
Denied,
Allowed,
};
#endif
[[nodiscard]] ErrorOr<void> install_no_new_privileges(); [[nodiscard]] ErrorOr<void> install_no_new_privileges();
[[nodiscard]] ErrorOr<void> configure_runtime(); [[nodiscard]] ErrorOr<void> configure_runtime();
#if defined(AK_OS_LINUX)
[[nodiscard]] ErrorOr<void> add_landlock_path_if_exists(Vector<LandlockPath>& paths, StringView path, LandlockPath::Access); [[nodiscard]] ErrorOr<void> add_landlock_path_if_exists(Vector<LandlockPath>& paths, StringView path, LandlockPath::Access);
[[nodiscard]] ErrorOr<void> restrict_filesystem_with_landlock(ReadonlySpan<LandlockPath>); [[nodiscard]] ErrorOr<void> restrict_filesystem_with_landlock(ReadonlySpan<LandlockPath>);
[[nodiscard]] ErrorOr<void> restrict_filesystem_with_landlock(ReadonlySpan<StringView> readable_paths = {}); [[nodiscard]] ErrorOr<void> restrict_filesystem_with_landlock(ReadonlySpan<StringView> readable_paths = {});
#endif
#if defined(AK_OS_MACOS)
[[nodiscard]] ErrorOr<void> add_seatbelt_path_if_exists(Vector<SeatbeltPath>& paths, StringView path, SeatbeltPath::Access);
[[nodiscard]] ErrorOr<void> apply_macos_sandbox(ReadonlySpan<SeatbeltPath>, NetworkAccess, ReadonlySpan<ByteString> executable_paths = {});
#endif
} }

View file

@ -22,6 +22,8 @@ add_executable(Compositor main.cpp)
if (LINUX) if (LINUX)
target_sources(Compositor PRIVATE SandboxLinux.cpp) target_sources(Compositor PRIVATE SandboxLinux.cpp)
elseif (APPLE)
target_sources(Compositor PRIVATE SandboxMacOS.cpp)
else() else()
target_sources(Compositor PRIVATE SandboxUnimplemented.cpp) target_sources(Compositor PRIVATE SandboxUnimplemented.cpp)
endif() endif()

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/LexicalPath.h>
#include <Compositor/Sandbox.h>
#include <LibCore/Directory.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/System.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibSandbox/Sandbox.h>
#include <LibWebView/Utilities.h>
#include <limits.h>
#include <string.h>
#include <unistd.h>
namespace Compositor {
ErrorOr<void> apply_sandbox()
{
TRY(Sandbox::configure_runtime());
auto executable_path = TRY(Core::System::current_executable_path());
auto build_root = LexicalPath::dirname(LexicalPath::dirname(LexicalPath::dirname(LexicalPath::dirname(LexicalPath::dirname(executable_path)))));
Vector<Sandbox::SeatbeltPath> paths;
TRY(Sandbox::add_seatbelt_path_if_exists(paths, executable_path, Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, LexicalPath::join(build_root, "bin"sv).string(), Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, LexicalPath::join(build_root, "lib"sv).string(), Sandbox::SeatbeltPath::Access::ReadAndExecute));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, LexicalPath::join(build_root, "vcpkg_installed"sv).string(), Sandbox::SeatbeltPath::Access::ReadAndExecute));
for (auto const& path : TRY(Gfx::FontDatabase::font_directories()))
TRY(Sandbox::add_seatbelt_path_if_exists(paths, path, Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, TRY(String::formatted("{}/fonts", WebView::s_ladybird_resource_root)), Sandbox::SeatbeltPath::Access::ReadOnly));
auto cache_path = Core::StandardPaths::cache_directory();
auto skia_cache_path = TRY(String::formatted("{}/Ladybird", cache_path));
TRY(Core::Directory::create(skia_cache_path.to_byte_string(), Core::Directory::CreateDirectories::Yes));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, skia_cache_path, Sandbox::SeatbeltPath::Access::ReadWrite));
char darwin_user_cache_directory[PATH_MAX];
if (confstr(_CS_DARWIN_USER_CACHE_DIR, darwin_user_cache_directory, sizeof(darwin_user_cache_directory)) > 0) {
StringView darwin_user_cache_directory_view { darwin_user_cache_directory, strlen(darwin_user_cache_directory) };
TRY(Sandbox::add_seatbelt_path_if_exists(paths, darwin_user_cache_directory_view, Sandbox::SeatbeltPath::Access::ReadWrite));
if (darwin_user_cache_directory_view.starts_with("/var/"sv)) {
auto private_darwin_user_cache_directory = TRY(String::formatted("/private{}", darwin_user_cache_directory));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, private_darwin_user_cache_directory, Sandbox::SeatbeltPath::Access::ReadWrite));
}
}
return Sandbox::apply_macos_sandbox(paths.span(), Sandbox::NetworkAccess::Denied);
}
}

View file

@ -24,6 +24,8 @@ if (LINUX)
if (ENABLE_ADDRESS_SANITIZER) if (ENABLE_ADDRESS_SANITIZER)
target_sources(ImageDecoder PRIVATE LeakSanitizer.cpp) target_sources(ImageDecoder PRIVATE LeakSanitizer.cpp)
endif() endif()
elseif (APPLE)
target_sources(ImageDecoder PRIVATE SandboxMacOS.cpp)
else() else()
target_sources(ImageDecoder PRIVATE SandboxUnimplemented.cpp) target_sources(ImageDecoder PRIVATE SandboxUnimplemented.cpp)
endif() endif()

View file

@ -0,0 +1,20 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <ImageDecoder/Sandbox.h>
#include <LibSandbox/Sandbox.h>
namespace ImageDecoder {
ErrorOr<void> apply_sandbox()
{
TRY(Sandbox::configure_runtime());
Vector<Sandbox::SeatbeltPath> paths;
return Sandbox::apply_macos_sandbox(paths.span(), Sandbox::NetworkAccess::Denied);
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/LexicalPath.h>
#include <LibCore/Directory.h>
#include <LibCore/Environment.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/System.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibSandbox/Sandbox.h>
#include <LibWebView/Utilities.h>
#include <Services/RendererSandbox.h>
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
namespace RendererSandbox {
static ErrorOr<Optional<ByteString>> canonicalized_path_if_exists(StringView path)
{
auto path_bytes = path.to_byte_string();
char resolved_path[PATH_MAX];
if (realpath(path_bytes.characters(), resolved_path) == nullptr) {
if (errno == ENOENT || errno == ENOTDIR)
return OptionalNone {};
return Error::from_syscall("realpath"sv, errno);
}
return ByteString { resolved_path };
}
ErrorOr<void> apply_sandbox(Optional<StringView> config_path)
{
TRY(Sandbox::configure_runtime());
auto executable_path = TRY(Core::System::current_executable_path());
auto build_root = LexicalPath::dirname(LexicalPath::dirname(LexicalPath::dirname(LexicalPath::dirname(LexicalPath::dirname(executable_path)))));
Vector<Sandbox::SeatbeltPath> paths;
TRY(Sandbox::add_seatbelt_path_if_exists(paths, WebView::s_ladybird_resource_root, Sandbox::SeatbeltPath::Access::ReadOnly));
if (config_path.has_value())
TRY(Sandbox::add_seatbelt_path_if_exists(paths, *config_path, Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, executable_path, Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, LexicalPath::join(build_root, "bin"sv).string(), Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, LexicalPath::join(build_root, "lib"sv).string(), Sandbox::SeatbeltPath::Access::ReadAndExecute));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, LexicalPath::join(build_root, "vcpkg_installed"sv).string(), Sandbox::SeatbeltPath::Access::ReadAndExecute));
for (auto const& path : TRY(Gfx::FontDatabase::font_directories()))
TRY(Sandbox::add_seatbelt_path_if_exists(paths, path, Sandbox::SeatbeltPath::Access::ReadOnly));
Vector<ByteString> executable_paths;
if (auto cranelift_compiler_path = Core::Environment::get("LADYBIRD_CRANELIFT_COMPILER"sv); cranelift_compiler_path.has_value()) {
TRY(Sandbox::add_seatbelt_path_if_exists(paths, *cranelift_compiler_path, Sandbox::SeatbeltPath::Access::ReadAndExecute));
if (auto canonicalized_cranelift_compiler_path = TRY(canonicalized_path_if_exists(*cranelift_compiler_path)); canonicalized_cranelift_compiler_path.has_value())
TRY(executable_paths.try_append(canonicalized_cranelift_compiler_path.release_value()));
} else {
auto default_cranelift_compiler_path = LexicalPath::join(build_root, "bin/cranelift-compiler"sv).string();
TRY(Sandbox::add_seatbelt_path_if_exists(paths, default_cranelift_compiler_path, Sandbox::SeatbeltPath::Access::ReadAndExecute));
if (auto canonicalized_cranelift_compiler_path = TRY(canonicalized_path_if_exists(default_cranelift_compiler_path)); canonicalized_cranelift_compiler_path.has_value())
TRY(executable_paths.try_append(canonicalized_cranelift_compiler_path.release_value()));
}
auto skia_cache_path = TRY(String::formatted("{}/Ladybird", Core::StandardPaths::cache_directory()));
TRY(Core::Directory::create(skia_cache_path.to_byte_string(), Core::Directory::CreateDirectories::Yes));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, skia_cache_path, Sandbox::SeatbeltPath::Access::ReadWrite));
char darwin_user_cache_directory[PATH_MAX];
if (confstr(_CS_DARWIN_USER_CACHE_DIR, darwin_user_cache_directory, sizeof(darwin_user_cache_directory)) > 0) {
StringView darwin_user_cache_directory_view { darwin_user_cache_directory, strlen(darwin_user_cache_directory) };
TRY(Sandbox::add_seatbelt_path_if_exists(paths, darwin_user_cache_directory_view, Sandbox::SeatbeltPath::Access::ReadWrite));
if (darwin_user_cache_directory_view.starts_with("/var/"sv)) {
auto private_darwin_user_cache_directory = TRY(String::formatted("/private{}", darwin_user_cache_directory));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, private_darwin_user_cache_directory, Sandbox::SeatbeltPath::Access::ReadWrite));
}
}
return Sandbox::apply_macos_sandbox(paths.span(), Sandbox::NetworkAccess::Denied, executable_paths.span());
}
}

View file

@ -10,6 +10,8 @@ set(SOURCES
if (LINUX) if (LINUX)
list(APPEND SOURCES SandboxLinux.cpp) list(APPEND SOURCES SandboxLinux.cpp)
elseif (APPLE)
list(APPEND SOURCES SandboxMacOS.cpp)
else() else()
list(APPEND SOURCES SandboxUnimplemented.cpp) list(APPEND SOURCES SandboxUnimplemented.cpp)
endif() endif()

View file

@ -87,4 +87,11 @@ Optional<ResourceSubstitution const&> ResourceSubstitutionMap::lookup(URL::URL c
return it->value; return it->value;
} }
ErrorOr<void> ResourceSubstitutionMap::for_each_substitution(Function<ErrorOr<void>(ResourceSubstitution const&)> const& callback) const
{
for (auto const& entry : m_substitutions)
TRY(callback(entry.value));
return {};
}
} }

View file

@ -7,9 +7,11 @@
#pragma once #pragma once
#include <AK/ByteString.h> #include <AK/ByteString.h>
#include <AK/Function.h>
#include <AK/HashMap.h> #include <AK/HashMap.h>
#include <AK/NonnullOwnPtr.h> #include <AK/NonnullOwnPtr.h>
#include <AK/Optional.h> #include <AK/Optional.h>
#include <AK/OwnPtr.h>
#include <AK/String.h> #include <AK/String.h>
#include <LibURL/URL.h> #include <LibURL/URL.h>
@ -26,6 +28,7 @@ public:
static ErrorOr<NonnullOwnPtr<ResourceSubstitutionMap>> load_from_file(StringView path); static ErrorOr<NonnullOwnPtr<ResourceSubstitutionMap>> load_from_file(StringView path);
Optional<ResourceSubstitution const&> lookup(URL::URL const&) const; Optional<ResourceSubstitution const&> lookup(URL::URL const&) const;
ErrorOr<void> for_each_substitution(Function<ErrorOr<void>(ResourceSubstitution const&)> const&) const;
private: private:
ResourceSubstitutionMap() = default; ResourceSubstitutionMap() = default;
@ -33,4 +36,6 @@ private:
HashMap<String, ResourceSubstitution> m_substitutions; HashMap<String, ResourceSubstitution> m_substitutions;
}; };
extern OwnPtr<ResourceSubstitutionMap> g_resource_substitution_map;
} }

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/LexicalPath.h>
#include <AK/String.h>
#include <LibCore/Directory.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/System.h>
#include <LibSandbox/Sandbox.h>
#include <RequestServer/ResourceSubstitutionMap.h>
#include <RequestServer/Sandbox.h>
namespace RequestServer {
ErrorOr<void> apply_sandbox(Vector<ByteString> const& certificates)
{
TRY(Sandbox::configure_runtime());
Vector<Sandbox::SeatbeltPath> paths;
auto cache_path = TRY(String::formatted("{}/Ladybird", Core::StandardPaths::cache_directory()));
TRY(Core::Directory::create(cache_path.to_byte_string(), Core::Directory::CreateDirectories::Yes));
auto executable_path = TRY(Core::System::current_executable_path());
auto build_root = LexicalPath::dirname(LexicalPath::dirname(LexicalPath::dirname(LexicalPath::dirname(LexicalPath::dirname(executable_path)))));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, executable_path, Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, LexicalPath::join(build_root, "bin"sv).string(), Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, LexicalPath::join(build_root, "lib"sv).string(), Sandbox::SeatbeltPath::Access::ReadAndExecute));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, LexicalPath::join(build_root, "vcpkg_installed"sv).string(), Sandbox::SeatbeltPath::Access::ReadAndExecute));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, "/etc/hosts"sv, Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, "/etc/resolv.conf"sv, Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, "/private/etc/hosts"sv, Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, "/private/etc/resolv.conf"sv, Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, "/private/etc/ssl"sv, Sandbox::SeatbeltPath::Access::ReadOnly));
TRY(Sandbox::add_seatbelt_path_if_exists(paths, "/Library/Preferences/com.apple.networkd.plist"sv, Sandbox::SeatbeltPath::Access::ReadOnly));
for (auto const& certificate : certificates) {
auto certificate_path = LexicalPath::dirname(certificate);
if (certificate_path.is_empty())
certificate_path = ".";
TRY(Sandbox::add_seatbelt_path_if_exists(paths, certificate_path, Sandbox::SeatbeltPath::Access::ReadOnly));
}
if (g_resource_substitution_map) {
TRY(g_resource_substitution_map->for_each_substitution([&](auto const& substitution) -> ErrorOr<void> {
TRY(Sandbox::add_seatbelt_path_if_exists(paths, substitution.file_path, Sandbox::SeatbeltPath::Access::ReadOnly));
return {};
}));
}
TRY(Sandbox::add_seatbelt_path_if_exists(paths, cache_path, Sandbox::SeatbeltPath::Access::ReadWrite));
return Sandbox::apply_macos_sandbox(paths.span(), Sandbox::NetworkAccess::Allowed);
}
}

View file

@ -40,6 +40,8 @@ add_executable(WebContent main.cpp)
if (LINUX) if (LINUX)
target_sources(WebContent PRIVATE ../RendererSandboxLinux.cpp) target_sources(WebContent PRIVATE ../RendererSandboxLinux.cpp)
elseif (APPLE)
target_sources(WebContent PRIVATE ../RendererSandboxMacOS.cpp)
else() else()
target_sources(WebContent PRIVATE ../RendererSandboxUnimplemented.cpp) target_sources(WebContent PRIVATE ../RendererSandboxUnimplemented.cpp)
endif() endif()

View file

@ -20,6 +20,8 @@ add_executable(WebWorker main.cpp)
if (LINUX) if (LINUX)
target_sources(WebWorker PRIVATE ../RendererSandboxLinux.cpp) target_sources(WebWorker PRIVATE ../RendererSandboxLinux.cpp)
elseif (APPLE)
target_sources(WebWorker PRIVATE ../RendererSandboxMacOS.cpp)
else() else()
target_sources(WebWorker PRIVATE ../RendererSandboxUnimplemented.cpp) target_sources(WebWorker PRIVATE ../RendererSandboxUnimplemented.cpp)
endif() endif()