From ddbc3e200642d365c64bcb6664fd2e0bd1e99aa0 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 10 Jun 2026 20:27:25 +0200 Subject: [PATCH] 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. --- Libraries/LibSandbox/Sandbox.cpp | 380 +++++++++++++++++- Libraries/LibSandbox/Sandbox.h | 30 ++ Services/Compositor/CMakeLists.txt | 2 + Services/Compositor/SandboxMacOS.cpp | 56 +++ Services/ImageDecoder/CMakeLists.txt | 2 + Services/ImageDecoder/SandboxMacOS.cpp | 20 + Services/RendererSandboxMacOS.cpp | 85 ++++ Services/RequestServer/CMakeLists.txt | 2 + .../RequestServer/ResourceSubstitutionMap.cpp | 7 + .../RequestServer/ResourceSubstitutionMap.h | 5 + Services/RequestServer/SandboxMacOS.cpp | 61 +++ Services/WebContent/CMakeLists.txt | 2 + Services/WebWorker/CMakeLists.txt | 2 + 13 files changed, 635 insertions(+), 19 deletions(-) create mode 100644 Services/Compositor/SandboxMacOS.cpp create mode 100644 Services/ImageDecoder/SandboxMacOS.cpp create mode 100644 Services/RendererSandboxMacOS.cpp create mode 100644 Services/RequestServer/SandboxMacOS.cpp diff --git a/Libraries/LibSandbox/Sandbox.cpp b/Libraries/LibSandbox/Sandbox.cpp index 702b0510e9..0dff64dcb5 100644 --- a/Libraries/LibSandbox/Sandbox.cpp +++ b/Libraries/LibSandbox/Sandbox.cpp @@ -19,6 +19,22 @@ # include #endif +#if defined(AK_OS_MACOS) +# include +# include +# include +# include +# include +# include +# include +# include +# include + +extern "C" { +int sandbox_init_with_parameters(char const* profile, u64 flags, char const* const parameters[], char** errorbuf); +} +#endif + #if defined(__GLIBC__) # include #endif @@ -43,9 +59,9 @@ ErrorOr configure_runtime() return {}; } +#if defined(AK_OS_LINUX) ErrorOr add_landlock_path_if_exists(Vector& paths, StringView path, LandlockPath::Access access) { -#if defined(AK_OS_LINUX) auto path_bytes = path.to_byte_string(); struct stat statbuf; @@ -64,17 +80,342 @@ ErrorOr add_landlock_path_if_exists(Vector& paths, StringVie } TRY(paths.try_append({ move(path_bytes), access })); -#else - (void)paths; - (void)path; - (void)access; + return {}; +} #endif + +#if defined(AK_OS_MACOS) +ErrorOr add_seatbelt_path_if_exists(Vector& 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(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 append_allowed_paths(StringBuilder& builder, StringView operation, ReadonlySpan 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 append_allowed_path_extensions(StringBuilder& builder, ReadonlySpan 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 append_allowed_executables(StringBuilder& builder, ReadonlySpan 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 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 apply_macos_sandbox(ReadonlySpan paths, NetworkAccess network_access, ReadonlySpan 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 restrict_filesystem_with_landlock(ReadonlySpan 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); if (landlock_abi < 0) { if (errno == ENOSYS || errno == EOPNOTSUPP || errno == EINVAL) @@ -99,19 +440,19 @@ ErrorOr restrict_filesystem_with_landlock(ReadonlySpan paths | LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM; -# ifdef LANDLOCK_ACCESS_FS_REFER +# ifdef LANDLOCK_ACCESS_FS_REFER if (landlock_abi >= 2) ruleset_attributes.handled_access_fs |= LANDLOCK_ACCESS_FS_REFER; -# endif -# ifdef LANDLOCK_ACCESS_FS_TRUNCATE +# 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) +# 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 +# else auto ruleset_attributes_size = sizeof(ruleset_attributes); -# endif +# 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); @@ -141,14 +482,14 @@ ErrorOr restrict_filesystem_with_landlock(ReadonlySpan paths | LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_FIFO; -# ifdef LANDLOCK_ACCESS_FS_REFER +# ifdef LANDLOCK_ACCESS_FS_REFER if (landlock_abi >= 2) path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_REFER; -# endif -# ifdef LANDLOCK_ACCESS_FS_TRUNCATE +# endif +# ifdef LANDLOCK_ACCESS_FS_TRUNCATE if (landlock_abi >= 3) path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_TRUNCATE; -# endif +# endif } path_beneath.parent_fd = path_fd; if (syscall(__NR_landlock_add_rule, ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0) < 0) @@ -157,9 +498,9 @@ ErrorOr restrict_filesystem_with_landlock(ReadonlySpan paths if (syscall(__NR_landlock_restrict_self, ruleset_fd, 0) < 0) return Error::from_syscall("landlock_restrict_self"sv, errno); -#else +# else (void)paths; -#endif +# endif return {}; } @@ -171,5 +512,6 @@ ErrorOr restrict_filesystem_with_landlock(ReadonlySpan readabl TRY(paths.try_append({ readable_path.to_byte_string(), LandlockPath::Access::ReadOnly })); return restrict_filesystem_with_landlock(paths.span()); } +#endif } diff --git a/Libraries/LibSandbox/Sandbox.h b/Libraries/LibSandbox/Sandbox.h index 960ba204a0..26fefb4042 100644 --- a/Libraries/LibSandbox/Sandbox.h +++ b/Libraries/LibSandbox/Sandbox.h @@ -8,12 +8,14 @@ #include #include +#include #include #include #include namespace Sandbox { +#if defined(AK_OS_LINUX) struct LandlockPath { enum class Access { ReadOnly, @@ -24,11 +26,39 @@ struct LandlockPath { ByteString path; 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 install_no_new_privileges(); [[nodiscard]] ErrorOr configure_runtime(); + +#if defined(AK_OS_LINUX) [[nodiscard]] ErrorOr add_landlock_path_if_exists(Vector& paths, StringView path, LandlockPath::Access); [[nodiscard]] ErrorOr restrict_filesystem_with_landlock(ReadonlySpan); [[nodiscard]] ErrorOr restrict_filesystem_with_landlock(ReadonlySpan readable_paths = {}); +#endif + +#if defined(AK_OS_MACOS) +[[nodiscard]] ErrorOr add_seatbelt_path_if_exists(Vector& paths, StringView path, SeatbeltPath::Access); +[[nodiscard]] ErrorOr apply_macos_sandbox(ReadonlySpan, NetworkAccess, ReadonlySpan executable_paths = {}); +#endif } diff --git a/Services/Compositor/CMakeLists.txt b/Services/Compositor/CMakeLists.txt index e6dee92202..837635ceef 100644 --- a/Services/Compositor/CMakeLists.txt +++ b/Services/Compositor/CMakeLists.txt @@ -22,6 +22,8 @@ add_executable(Compositor main.cpp) if (LINUX) target_sources(Compositor PRIVATE SandboxLinux.cpp) +elseif (APPLE) + target_sources(Compositor PRIVATE SandboxMacOS.cpp) else() target_sources(Compositor PRIVATE SandboxUnimplemented.cpp) endif() diff --git a/Services/Compositor/SandboxMacOS.cpp b/Services/Compositor/SandboxMacOS.cpp new file mode 100644 index 0000000000..3257b4fa8d --- /dev/null +++ b/Services/Compositor/SandboxMacOS.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Compositor { + +ErrorOr 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 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); +} + +} diff --git a/Services/ImageDecoder/CMakeLists.txt b/Services/ImageDecoder/CMakeLists.txt index be8343e469..d5831cba0f 100644 --- a/Services/ImageDecoder/CMakeLists.txt +++ b/Services/ImageDecoder/CMakeLists.txt @@ -24,6 +24,8 @@ if (LINUX) if (ENABLE_ADDRESS_SANITIZER) target_sources(ImageDecoder PRIVATE LeakSanitizer.cpp) endif() +elseif (APPLE) + target_sources(ImageDecoder PRIVATE SandboxMacOS.cpp) else() target_sources(ImageDecoder PRIVATE SandboxUnimplemented.cpp) endif() diff --git a/Services/ImageDecoder/SandboxMacOS.cpp b/Services/ImageDecoder/SandboxMacOS.cpp new file mode 100644 index 0000000000..135e74b00f --- /dev/null +++ b/Services/ImageDecoder/SandboxMacOS.cpp @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace ImageDecoder { + +ErrorOr apply_sandbox() +{ + TRY(Sandbox::configure_runtime()); + + Vector paths; + return Sandbox::apply_macos_sandbox(paths.span(), Sandbox::NetworkAccess::Denied); +} + +} diff --git a/Services/RendererSandboxMacOS.cpp b/Services/RendererSandboxMacOS.cpp new file mode 100644 index 0000000000..b4818ddefd --- /dev/null +++ b/Services/RendererSandboxMacOS.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RendererSandbox { + +static ErrorOr> 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 apply_sandbox(Optional 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 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 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()); +} + +} diff --git a/Services/RequestServer/CMakeLists.txt b/Services/RequestServer/CMakeLists.txt index 7953950cb3..723b507bcc 100644 --- a/Services/RequestServer/CMakeLists.txt +++ b/Services/RequestServer/CMakeLists.txt @@ -10,6 +10,8 @@ set(SOURCES if (LINUX) list(APPEND SOURCES SandboxLinux.cpp) +elseif (APPLE) + list(APPEND SOURCES SandboxMacOS.cpp) else() list(APPEND SOURCES SandboxUnimplemented.cpp) endif() diff --git a/Services/RequestServer/ResourceSubstitutionMap.cpp b/Services/RequestServer/ResourceSubstitutionMap.cpp index 9ba7602bdf..63371e09e6 100644 --- a/Services/RequestServer/ResourceSubstitutionMap.cpp +++ b/Services/RequestServer/ResourceSubstitutionMap.cpp @@ -87,4 +87,11 @@ Optional ResourceSubstitutionMap::lookup(URL::URL c return it->value; } +ErrorOr ResourceSubstitutionMap::for_each_substitution(Function(ResourceSubstitution const&)> const& callback) const +{ + for (auto const& entry : m_substitutions) + TRY(callback(entry.value)); + return {}; +} + } diff --git a/Services/RequestServer/ResourceSubstitutionMap.h b/Services/RequestServer/ResourceSubstitutionMap.h index 6b0f87474f..8be6d2787f 100644 --- a/Services/RequestServer/ResourceSubstitutionMap.h +++ b/Services/RequestServer/ResourceSubstitutionMap.h @@ -7,9 +7,11 @@ #pragma once #include +#include #include #include #include +#include #include #include @@ -26,6 +28,7 @@ public: static ErrorOr> load_from_file(StringView path); Optional lookup(URL::URL const&) const; + ErrorOr for_each_substitution(Function(ResourceSubstitution const&)> const&) const; private: ResourceSubstitutionMap() = default; @@ -33,4 +36,6 @@ private: HashMap m_substitutions; }; +extern OwnPtr g_resource_substitution_map; + } diff --git a/Services/RequestServer/SandboxMacOS.cpp b/Services/RequestServer/SandboxMacOS.cpp new file mode 100644 index 0000000000..d429f26c5e --- /dev/null +++ b/Services/RequestServer/SandboxMacOS.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RequestServer { + +ErrorOr apply_sandbox(Vector const& certificates) +{ + TRY(Sandbox::configure_runtime()); + + Vector 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 { + 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); +} + +} diff --git a/Services/WebContent/CMakeLists.txt b/Services/WebContent/CMakeLists.txt index 06d8ae6bc3..7aea6f54a1 100644 --- a/Services/WebContent/CMakeLists.txt +++ b/Services/WebContent/CMakeLists.txt @@ -40,6 +40,8 @@ add_executable(WebContent main.cpp) if (LINUX) target_sources(WebContent PRIVATE ../RendererSandboxLinux.cpp) +elseif (APPLE) + target_sources(WebContent PRIVATE ../RendererSandboxMacOS.cpp) else() target_sources(WebContent PRIVATE ../RendererSandboxUnimplemented.cpp) endif() diff --git a/Services/WebWorker/CMakeLists.txt b/Services/WebWorker/CMakeLists.txt index 418e004b71..0b9390c994 100644 --- a/Services/WebWorker/CMakeLists.txt +++ b/Services/WebWorker/CMakeLists.txt @@ -20,6 +20,8 @@ add_executable(WebWorker main.cpp) if (LINUX) target_sources(WebWorker PRIVATE ../RendererSandboxLinux.cpp) +elseif (APPLE) + target_sources(WebWorker PRIVATE ../RendererSandboxMacOS.cpp) else() target_sources(WebWorker PRIVATE ../RendererSandboxUnimplemented.cpp) endif()