From 0c7fda8735f4c34ffe271aee50dbab3237c3af47 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 9 Jun 2026 13:26:47 +0200 Subject: [PATCH] WebContent: Sandbox renderer processes on Linux Add opt-in Linux renderer sandbox support to WebContent and WebWorker. Ladybird and test-web pass --enable-sandbox through when requested, and the renderer services only install the shared sandbox when that flag is present. Share one renderer policy for both services. Allow resource, font, shared library, WebGL, Wasm, audio, and local IPC paths needed at runtime, while keeping renderer filesystem writes mediated by Landlock. Allow Mesa and PulseAudio to probe their standard runtime state without escaping the renderer sandbox. Return EPERM for scheduler and priority changes so library initialization can fall back instead of crashing on a seccomp violation. --- Libraries/LibSandbox/Sandbox.cpp | 2 + Libraries/LibSandbox/Sandbox.h | 1 + Libraries/LibSandbox/Seccomp.cpp | 159 ++++++++++++++++++++++ Libraries/LibSandbox/Seccomp.h | 2 + Libraries/LibWebView/HelperProcess.cpp | 5 + Services/RendererSandbox.h | 17 +++ Services/RendererSandboxLinux.cpp | 88 ++++++++++++ Services/RendererSandboxUnimplemented.cpp | 16 +++ Services/WebContent/CMakeLists.txt | 8 +- Services/WebContent/main.cpp | 6 + Services/WebWorker/CMakeLists.txt | 8 +- Services/WebWorker/main.cpp | 6 + 12 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 Services/RendererSandbox.h create mode 100644 Services/RendererSandboxLinux.cpp create mode 100644 Services/RendererSandboxUnimplemented.cpp diff --git a/Libraries/LibSandbox/Sandbox.cpp b/Libraries/LibSandbox/Sandbox.cpp index acb8178e0e..702b0510e9 100644 --- a/Libraries/LibSandbox/Sandbox.cpp +++ b/Libraries/LibSandbox/Sandbox.cpp @@ -131,6 +131,8 @@ ErrorOr restrict_filesystem_with_landlock(ReadonlySpan paths landlock_path_beneath_attr path_beneath {}; path_beneath.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR; + if (landlock_path.access == LandlockPath::Access::ReadAndExecute) + path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_EXECUTE; if (landlock_path.access == LandlockPath::Access::ReadWrite) { path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_REMOVE_DIR diff --git a/Libraries/LibSandbox/Sandbox.h b/Libraries/LibSandbox/Sandbox.h index f3741c2edf..960ba204a0 100644 --- a/Libraries/LibSandbox/Sandbox.h +++ b/Libraries/LibSandbox/Sandbox.h @@ -17,6 +17,7 @@ namespace Sandbox { struct LandlockPath { enum class Access { ReadOnly, + ReadAndExecute, ReadWrite, }; diff --git a/Libraries/LibSandbox/Seccomp.cpp b/Libraries/LibSandbox/Seccomp.cpp index 238b865668..78519ea169 100644 --- a/Libraries/LibSandbox/Seccomp.cpp +++ b/Libraries/LibSandbox/Seccomp.cpp @@ -10,10 +10,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -40,6 +42,7 @@ static constexpr u32 thread_clone_allowed_flags = thread_clone_required_flags | CLONE_CHILD_CLEARTID | CLONE_DETACHED | CLONE_CHILD_SETTID; +static constexpr u32 vfork_clone_flags = CLONE_VM | CLONE_VFORK | SIGCHLD; #ifdef O_LARGEFILE static constexpr unsigned read_only_open_flags = O_CLOEXEC | O_LARGEFILE; @@ -81,6 +84,8 @@ static constexpr unsigned read_only_open_flags = O_CLOEXEC; #define IF_DEFINED_epoll_pwait(if_defined, if_not_defined) if_defined #define IF_DEFINED_epoll_wait(if_defined, if_not_defined) if_defined #define IF_DEFINED_eventfd2(if_defined, if_not_defined) if_defined +#define IF_DEFINED_execve(if_defined, if_not_defined) if_defined +#define IF_DEFINED_execveat(if_defined, if_not_defined) if_defined #define IF_DEFINED_exit(if_defined, if_not_defined) if_defined #define IF_DEFINED_exit_group(if_defined, if_not_defined) if_defined #define IF_DEFINED_fcntl(if_defined, if_not_defined) if_defined @@ -115,6 +120,8 @@ static constexpr unsigned read_only_open_flags = O_CLOEXEC; #define IF_DEFINED_madvise(if_defined, if_not_defined) if_defined #define IF_DEFINED_membarrier(if_defined, if_not_defined) if_defined #define IF_DEFINED_memfd_create(if_defined, if_not_defined) if_defined +#define IF_DEFINED_mkdir(if_defined, if_not_defined) if_defined +#define IF_DEFINED_mkdirat(if_defined, if_not_defined) if_defined #define IF_DEFINED_mmap(if_defined, if_not_defined) if_defined #define IF_DEFINED_mmap2(if_defined, if_not_defined) if_defined #define IF_DEFINED_mprotect(if_defined, if_not_defined) if_defined @@ -142,6 +149,7 @@ static constexpr unsigned read_only_open_flags = O_CLOEXEC; #define IF_DEFINED_recvmmsg(if_defined, if_not_defined) if_defined #define IF_DEFINED_recvmsg(if_defined, if_not_defined) if_defined #define IF_DEFINED_restart_syscall(if_defined, if_not_defined) if_defined +#define IF_DEFINED_rmdir(if_defined, if_not_defined) if_defined #define IF_DEFINED_rseq(if_defined, if_not_defined) if_defined #define IF_DEFINED_rt_sigaction(if_defined, if_not_defined) if_defined #define IF_DEFINED_rt_sigprocmask(if_defined, if_not_defined) if_defined @@ -166,6 +174,8 @@ static constexpr unsigned read_only_open_flags = O_CLOEXEC; #define IF_DEFINED_unlinkat(if_defined, if_not_defined) if_defined #define IF_DEFINED_umask(if_defined, if_not_defined) if_defined #define IF_DEFINED_uname(if_defined, if_not_defined) if_defined +#define IF_DEFINED_wait4(if_defined, if_not_defined) if_defined +#define IF_DEFINED_waitid(if_defined, if_not_defined) if_defined #define IF_DEFINED_write(if_defined, if_not_defined) if_defined #ifndef __NR_accept @@ -228,6 +238,14 @@ static constexpr unsigned read_only_open_flags = O_CLOEXEC; # undef IF_DEFINED_eventfd2 # define IF_DEFINED_eventfd2(if_defined, if_not_defined) if_not_defined #endif +#ifndef __NR_execve +# undef IF_DEFINED_execve +# define IF_DEFINED_execve(if_defined, if_not_defined) if_not_defined +#endif +#ifndef __NR_execveat +# undef IF_DEFINED_execveat +# define IF_DEFINED_execveat(if_defined, if_not_defined) if_not_defined +#endif #ifndef __NR_fcntl64 # undef IF_DEFINED_fcntl64 # define IF_DEFINED_fcntl64(if_defined, if_not_defined) if_not_defined @@ -284,6 +302,14 @@ static constexpr unsigned read_only_open_flags = O_CLOEXEC; # undef IF_DEFINED_memfd_create # define IF_DEFINED_memfd_create(if_defined, if_not_defined) if_not_defined #endif +#ifndef __NR_mkdir +# undef IF_DEFINED_mkdir +# define IF_DEFINED_mkdir(if_defined, if_not_defined) if_not_defined +#endif +#ifndef __NR_mkdirat +# undef IF_DEFINED_mkdirat +# define IF_DEFINED_mkdirat(if_defined, if_not_defined) if_not_defined +#endif #ifndef __NR_mmap2 # undef IF_DEFINED_mmap2 # define IF_DEFINED_mmap2(if_defined, if_not_defined) if_not_defined @@ -332,6 +358,10 @@ static constexpr unsigned read_only_open_flags = O_CLOEXEC; # undef IF_DEFINED_renameat2 # define IF_DEFINED_renameat2(if_defined, if_not_defined) if_not_defined #endif +#ifndef __NR_rmdir +# undef IF_DEFINED_rmdir +# define IF_DEFINED_rmdir(if_defined, if_not_defined) if_not_defined +#endif #ifndef __NR_ppoll # undef IF_DEFINED_ppoll # define IF_DEFINED_ppoll(if_defined, if_not_defined) if_not_defined @@ -400,6 +430,14 @@ static constexpr unsigned read_only_open_flags = O_CLOEXEC; # undef IF_DEFINED_umask # define IF_DEFINED_umask(if_defined, if_not_defined) if_not_defined #endif +#ifndef __NR_wait4 +# undef IF_DEFINED_wait4 +# define IF_DEFINED_wait4(if_defined, if_not_defined) if_not_defined +#endif +#ifndef __NR_waitid +# undef IF_DEFINED_waitid +# define IF_DEFINED_waitid(if_defined, if_not_defined) if_not_defined +#endif static size_t safe_string_length(char const* string) { @@ -736,6 +774,9 @@ void SeccompPolicy::allow_filesystem_writes() SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, rename); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, renameat); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, renameat2); + SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, mkdir); + SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, mkdirat); + SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, rmdir); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, fsync); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, fdatasync); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, flock); @@ -806,6 +847,31 @@ void SeccompPolicy::allow_file_descriptor_operations() append(SECCOMP_LOAD_SYSCALL_NR); append(BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 0)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fcntl, 0, 5)); + append(SECCOMP_LOAD_ARGUMENT(1)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, F_DUPFD_CLOEXEC, 0, 1)); + append(SECCOMP_ALLOW); + append(SECCOMP_LOAD_SYSCALL_NR); + append(BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 0)); + +#ifdef F_ADD_SEALS + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fcntl, 0, 5)); + append(SECCOMP_LOAD_ARGUMENT(1)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, F_ADD_SEALS, 0, 1)); + append(SECCOMP_ALLOW); + append(SECCOMP_LOAD_SYSCALL_NR); + append(BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 0)); +#endif + +#ifdef F_GET_SEALS + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fcntl, 0, 5)); + append(SECCOMP_LOAD_ARGUMENT(1)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, F_GET_SEALS, 0, 1)); + append(SECCOMP_ALLOW); + append(SECCOMP_LOAD_SYSCALL_NR); + append(BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 0)); +#endif + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_ioctl, 0, 5)); append(SECCOMP_LOAD_ARGUMENT(1)); append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, FIONBIO, 0, 1)); @@ -828,6 +894,23 @@ void SeccompPolicy::allow_file_descriptor_operations() append(BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 0)); } +void SeccompPolicy::allow_process_creation() +{ +#ifdef __NR_clone + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_clone, 0, 5)); + append(SECCOMP_LOAD_ARGUMENT(0)); + append(BPF_STMT(BPF_ALU | BPF_AND | BPF_K, ~vfork_clone_flags)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 0, 1)); + append(SECCOMP_ALLOW); + append(SECCOMP_LOAD_SYSCALL_NR); +#endif + + SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, execve); + SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, execveat); + SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, wait4); + SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, waitid); +} + void SeccompPolicy::allow_ipc() { SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, eventfd2); @@ -841,6 +924,14 @@ void SeccompPolicy::allow_ipc() SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, sendto); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, sendmmsg); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, socketpair); +#ifdef __NR_socket + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_socket, 0, 3)); + append(SECCOMP_LOAD_ARGUMENT(0)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_UNIX, 0, 1)); + append(SECCOMP_ALLOW); + append(SECCOMP_LOAD_SYSCALL_NR); +#endif + SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, connect); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, getsockopt); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, setsockopt); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, getsockname); @@ -889,6 +980,41 @@ void SeccompPolicy::allow_memory_without_executable_mappings() SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, brk); } +void SeccompPolicy::allow_executable_memory_mappings() +{ + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mmap, 0, 8)); + append(SECCOMP_LOAD_ARGUMENT(2)); + append(BPF_STMT(BPF_ALU | BPF_AND | BPF_K, PROT_EXEC)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 4, 0)); + append(SECCOMP_LOAD_ARGUMENT(2)); + append(BPF_STMT(BPF_ALU | BPF_AND | BPF_K, PROT_WRITE)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 0, 1)); + append(SECCOMP_ALLOW); + append(SECCOMP_LOAD_SYSCALL_NR); + +#ifdef __NR_mmap2 + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mmap2, 0, 8)); + append(SECCOMP_LOAD_ARGUMENT(2)); + append(BPF_STMT(BPF_ALU | BPF_AND | BPF_K, PROT_EXEC)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 4, 0)); + append(SECCOMP_LOAD_ARGUMENT(2)); + append(BPF_STMT(BPF_ALU | BPF_AND | BPF_K, PROT_WRITE)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 0, 1)); + append(SECCOMP_ALLOW); + append(SECCOMP_LOAD_SYSCALL_NR); +#endif + + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mprotect, 0, 8)); + append(SECCOMP_LOAD_ARGUMENT(2)); + append(BPF_STMT(BPF_ALU | BPF_AND | BPF_K, PROT_EXEC)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 4, 0)); + append(SECCOMP_LOAD_ARGUMENT(2)); + append(BPF_STMT(BPF_ALU | BPF_AND | BPF_K, PROT_WRITE)); + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 0, 1)); + append(SECCOMP_ALLOW); + append(SECCOMP_LOAD_SYSCALL_NR); +} + void SeccompPolicy::allow_threads() { #ifdef __NR_clone @@ -961,6 +1087,39 @@ void SeccompPolicy::allow_process_metadata() SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, sysinfo); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, uname); SECCOMP_APPEND_ALLOW_SYSCALL_IF_DEFINED(*this, umask); + +#ifdef __NR_sched_getscheduler + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_sched_getscheduler, 0, 1)); + append(SECCOMP_ALLOW); +#endif +#ifdef __NR_sched_get_priority_max + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_sched_get_priority_max, 0, 1)); + append(SECCOMP_ALLOW); +#endif +#ifdef __NR_sched_get_priority_min + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_sched_get_priority_min, 0, 1)); + append(SECCOMP_ALLOW); +#endif +#ifdef __NR_getpriority + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpriority, 0, 1)); + append(SECCOMP_ALLOW); +#endif +#ifdef __NR_setpriority + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_setpriority, 0, 1)); + append(SECCOMP_ERRNO(EPERM)); +#endif +#ifdef __NR_sched_setscheduler + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_sched_setscheduler, 0, 1)); + append(SECCOMP_ERRNO(EPERM)); +#endif +#ifdef __NR_sched_setparam + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_sched_setparam, 0, 1)); + append(SECCOMP_ERRNO(EPERM)); +#endif +#ifdef __NR_sched_setaffinity + append(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_sched_setaffinity, 0, 1)); + append(SECCOMP_ERRNO(EPERM)); +#endif } void SeccompPolicy::allow_common_runtime() diff --git a/Libraries/LibSandbox/Seccomp.h b/Libraries/LibSandbox/Seccomp.h index 76bb62d0cf..8e123d00cd 100644 --- a/Libraries/LibSandbox/Seccomp.h +++ b/Libraries/LibSandbox/Seccomp.h @@ -21,9 +21,11 @@ public: void allow_filesystem_metadata_queries(); void allow_filesystem_writes(); void allow_file_descriptor_operations(); + void allow_process_creation(); void allow_ipc(); void allow_network(); void allow_memory_without_executable_mappings(); + void allow_executable_memory_mappings(); void allow_threads(); void allow_signals(); void allow_clocks(); diff --git a/Libraries/LibWebView/HelperProcess.cpp b/Libraries/LibWebView/HelperProcess.cpp index ba3b8a9a9a..c25818a490 100644 --- a/Libraries/LibWebView/HelperProcess.cpp +++ b/Libraries/LibWebView/HelperProcess.cpp @@ -122,6 +122,8 @@ ErrorOr> launch_web_content_process(u64 arguments.append("--disable-async-scrolling"sv); if (web_content_options.file_scheme_urls_have_tuple_origins == FileSchemeUrlsHaveTupleOrigins::Yes) arguments.append("--tuple-file-origins"sv); + if (browser_options.enable_sandbox == EnableSandbox::Yes) + arguments.append("--enable-sandbox"sv); if (auto const maybe_echo_server_port = web_content_options.echo_server_port; maybe_echo_server_port.has_value()) { arguments.append("--echo-server-port"sv); @@ -183,10 +185,13 @@ ErrorOr> launch_compositor_process() ErrorOr> launch_web_worker_process(Web::Bindings::AgentType type, Web::HTML::WorkerAgentId agent_id) { + auto const& browser_options = WebView::Application::browser_options(); auto const& web_content_options = WebView::Application::web_content_options(); Vector arguments; + if (browser_options.enable_sandbox == EnableSandbox::Yes) + arguments.append("--enable-sandbox"sv); if (web_content_options.expose_experimental_interfaces == WebView::ExposeExperimentalInterfaces::Yes) arguments.append("--expose-experimental-interfaces"sv); if (web_content_options.enable_http_memory_cache == WebView::EnableMemoryHTTPCache::Yes) diff --git a/Services/RendererSandbox.h b/Services/RendererSandbox.h new file mode 100644 index 0000000000..4e7e901341 --- /dev/null +++ b/Services/RendererSandbox.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace RendererSandbox { + +[[nodiscard]] ErrorOr apply_sandbox(Optional config_path); + +} diff --git a/Services/RendererSandboxLinux.cpp b/Services/RendererSandboxLinux.cpp new file mode 100644 index 0000000000..61188f8204 --- /dev/null +++ b/Services/RendererSandboxLinux.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RendererSandbox { + +ErrorOr apply_sandbox(Optional config_path) +{ + TRY(Sandbox::install_no_new_privileges()); + TRY(Sandbox::configure_runtime()); + + auto executable_path = TRY(Core::System::current_executable_path()); + auto build_root = LexicalPath::dirname(LexicalPath::dirname(executable_path)); + + Vector paths; + TRY(Sandbox::add_landlock_path_if_exists(paths, WebView::s_ladybird_resource_root, Sandbox::LandlockPath::Access::ReadOnly)); + if (config_path.has_value()) + TRY(Sandbox::add_landlock_path_if_exists(paths, *config_path, Sandbox::LandlockPath::Access::ReadOnly)); + // cpptrace opens loaded ELF objects when symbolizing in-process stack traces. + TRY(Sandbox::add_landlock_path_if_exists(paths, executable_path, Sandbox::LandlockPath::Access::ReadOnly)); + TRY(Sandbox::add_landlock_path_if_exists(paths, LexicalPath::join(build_root, "lib"sv).string(), Sandbox::LandlockPath::Access::ReadOnly)); + TRY(Sandbox::add_landlock_path_if_exists(paths, "/proc/self"sv, Sandbox::LandlockPath::Access::ReadOnly)); + TRY(Sandbox::add_landlock_path_if_exists(paths, "/lib"sv, Sandbox::LandlockPath::Access::ReadOnly)); + TRY(Sandbox::add_landlock_path_if_exists(paths, "/lib64"sv, Sandbox::LandlockPath::Access::ReadOnly)); + TRY(Sandbox::add_landlock_path_if_exists(paths, "/usr/lib"sv, Sandbox::LandlockPath::Access::ReadOnly)); + TRY(Sandbox::add_landlock_path_if_exists(paths, "/usr/local/lib"sv, Sandbox::LandlockPath::Access::ReadOnly)); + TRY(Sandbox::add_landlock_path_if_exists(paths, "/etc/glvnd"sv, Sandbox::LandlockPath::Access::ReadOnly)); + TRY(Sandbox::add_landlock_path_if_exists(paths, "/usr/share/glvnd"sv, Sandbox::LandlockPath::Access::ReadOnly)); + TRY(Sandbox::add_landlock_path_if_exists(paths, "/usr/share/drirc.d"sv, Sandbox::LandlockPath::Access::ReadOnly)); + TRY(Sandbox::add_landlock_path_if_exists(paths, "/usr/share/vulkan"sv, Sandbox::LandlockPath::Access::ReadOnly)); + TRY(Sandbox::add_landlock_path_if_exists(paths, "/dev/dri"sv, Sandbox::LandlockPath::Access::ReadWrite)); + TRY(Sandbox::add_landlock_path_if_exists(paths, "/sys"sv, Sandbox::LandlockPath::Access::ReadOnly)); + if (auto library_path = Core::Environment::get("LD_LIBRARY_PATH"sv); library_path.has_value()) { + for (auto path : library_path->split_view(':')) + TRY(Sandbox::add_landlock_path_if_exists(paths, path, Sandbox::LandlockPath::Access::ReadOnly)); + } + for (auto const& path : TRY(Gfx::FontDatabase::font_directories())) + TRY(Sandbox::add_landlock_path_if_exists(paths, path, Sandbox::LandlockPath::Access::ReadOnly)); + + if (auto cranelift_compiler_path = Core::Environment::get("LADYBIRD_CRANELIFT_COMPILER"sv); cranelift_compiler_path.has_value()) { + TRY(Sandbox::add_landlock_path_if_exists(paths, *cranelift_compiler_path, Sandbox::LandlockPath::Access::ReadAndExecute)); + } else { + auto default_cranelift_compiler_path = LexicalPath::join(build_root, "bin/cranelift-compiler"sv).string(); + TRY(Sandbox::add_landlock_path_if_exists(paths, default_cranelift_compiler_path, Sandbox::LandlockPath::Access::ReadAndExecute)); + } + + auto mesa_shader_cache_path = Core::Environment::get("MESA_SHADER_CACHE_DIR"sv) + .map([](auto path) { return path.to_byte_string(); }) + .value_or_lazy_evaluated([] { return ByteString::formatted("{}/mesa_shader_cache", Core::StandardPaths::cache_directory()); }); + TRY(Core::Directory::create(mesa_shader_cache_path, Core::Directory::CreateDirectories::Yes)); + TRY(Sandbox::add_landlock_path_if_exists(paths, mesa_shader_cache_path, Sandbox::LandlockPath::Access::ReadWrite)); + + auto pulse_runtime_path = LexicalPath::join(TRY(Core::StandardPaths::runtime_directory()), "pulse"sv).string(); + TRY(Core::Directory::create(pulse_runtime_path, Core::Directory::CreateDirectories::Yes, 0700)); + TRY(Sandbox::add_landlock_path_if_exists(paths, pulse_runtime_path, Sandbox::LandlockPath::Access::ReadWrite)); + TRY(Sandbox::add_landlock_path_if_exists(paths, LexicalPath::join(Core::StandardPaths::config_directory(), "pulse"sv).string(), Sandbox::LandlockPath::Access::ReadOnly)); + + TRY(Sandbox::restrict_filesystem_with_landlock(paths.span())); + + Sandbox::SeccompPolicy policy; + policy.allow_readonly_file_opens(); + policy.allow_filesystem_metadata_queries(); + policy.allow_filesystem_writes(); + policy.allow_file_descriptor_operations(); + policy.allow_process_creation(); + policy.allow_ipc(); + policy.allow_gpu_device_operations(); + policy.allow_common_runtime(); + policy.allow_executable_memory_mappings(); + TRY(policy.install()); + + return {}; +} + +} diff --git a/Services/RendererSandboxUnimplemented.cpp b/Services/RendererSandboxUnimplemented.cpp new file mode 100644 index 0000000000..e26bf955ca --- /dev/null +++ b/Services/RendererSandboxUnimplemented.cpp @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace RendererSandbox { + +ErrorOr apply_sandbox(Optional) +{ + return {}; +} + +} diff --git a/Services/WebContent/CMakeLists.txt b/Services/WebContent/CMakeLists.txt index f353ca14be..06d8ae6bc3 100644 --- a/Services/WebContent/CMakeLists.txt +++ b/Services/WebContent/CMakeLists.txt @@ -38,7 +38,13 @@ target_compile_options(webcontentservice PRIVATE $<$:-Wexit-time-destructors>) if(WIN32) diff --git a/Services/WebContent/main.cpp b/Services/WebContent/main.cpp index ec436d09a6..2762f8faa7 100644 --- a/Services/WebContent/main.cpp +++ b/Services/WebContent/main.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -150,6 +151,7 @@ ErrorOr ladybird_main(Main::Arguments arguments) bool is_headless = false; bool disable_scrollbar_painting = false; bool disable_async_scrolling = false; + bool enable_sandbox = false; StringView echo_server_port_string_view {}; StringView default_time_zone {}; StringView style_invalidation_counter_dump_interval {}; @@ -172,6 +174,7 @@ ErrorOr ladybird_main(Main::Arguments arguments) args_parser.add_option(collect_garbage_on_every_allocation, "Collect garbage after every JS heap allocation", "collect-garbage-on-every-allocation"); args_parser.add_option(disable_scrollbar_painting, "Don't paint horizontal or vertical viewport scrollbars", "disable-scrollbar-painting"); args_parser.add_option(disable_async_scrolling, "Disable async scrolling", "disable-async-scrolling"); + args_parser.add_option(enable_sandbox, "Enable process sandboxing", "enable-sandbox"); args_parser.add_option(echo_server_port_string_view, "Echo server port used in test internals", "echo-server-port", 0, "echo_server_port"); args_parser.add_option(is_headless, "Report that the browser is running in headless mode", "headless"); args_parser.add_option(default_time_zone, "Default time zone", "default-time-zone", 0, "time-zone-id"); @@ -256,6 +259,9 @@ ErrorOr ladybird_main(Main::Arguments arguments) if (maybe_content_blocker_error.is_error()) dbgln("Failed to load content blockers: {}", maybe_content_blocker_error.error()); + if (enable_sandbox) + TRY(RendererSandbox::apply_sandbox(config_path)); + #if defined(AK_OS_MACOS) auto browser_port = TRY(Core::MachPort::look_up_from_bootstrap_server(ByteString { mach_server_name })); auto transport_ports = TRY(IPC::bootstrap_transport_from_server_port(browser_port)); diff --git a/Services/WebWorker/CMakeLists.txt b/Services/WebWorker/CMakeLists.txt index 6b24cd1c23..418e004b71 100644 --- a/Services/WebWorker/CMakeLists.txt +++ b/Services/WebWorker/CMakeLists.txt @@ -18,8 +18,14 @@ target_link_libraries(webworkerservice PRIVATE OpenSSL::Crypto OpenSSL::SSL) add_executable(WebWorker main.cpp) +if (LINUX) + target_sources(WebWorker PRIVATE ../RendererSandboxLinux.cpp) +else() + target_sources(WebWorker PRIVATE ../RendererSandboxUnimplemented.cpp) +endif() + target_include_directories(WebWorker PRIVATE ${LADYBIRD_SOURCE_DIR}) -target_link_libraries(WebWorker PRIVATE webworkerservice OpenSSL::Crypto OpenSSL::SSL) +target_link_libraries(WebWorker PRIVATE webworkerservice LibSandbox OpenSSL::Crypto OpenSSL::SSL) if(WIN32) target_include_directories(WebWorker PRIVATE $) diff --git a/Services/WebWorker/main.cpp b/Services/WebWorker/main.cpp index 062b9f61d7..adb62ad6c9 100644 --- a/Services/WebWorker/main.cpp +++ b/Services/WebWorker/main.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -56,6 +57,7 @@ ErrorOr ladybird_main(Main::Arguments arguments) bool enable_http_memory_cache = false; bool wait_for_debugger = false; bool file_origins_are_tuple_origins = false; + bool enable_sandbox = false; Core::ArgsParser args_parser; args_parser.add_option(serenity_resource_root, "Absolute path to directory for serenity resources", "serenity-resource-root", 'r', "serenity-resource-root"); @@ -66,6 +68,7 @@ ErrorOr ladybird_main(Main::Arguments arguments) args_parser.add_option(worker_type_string, "Type of WebWorker to start (dedicated, shared, or service)", "type", 't', "type"); args_parser.add_option(mach_server_name, "Mach server name", "mach-server-name", 0, "mach_server_name"); args_parser.add_option(file_origins_are_tuple_origins, "Treat file:// URLs as having tuple origins", "tuple-file-origins"); + args_parser.add_option(enable_sandbox, "Enable process sandboxing", "enable-sandbox"); args_parser.parse(arguments); @@ -94,6 +97,9 @@ ErrorOr ladybird_main(Main::Arguments arguments) Web::Bindings::initialize_main_thread_vm(worker_type); + if (enable_sandbox) + TRY(RendererSandbox::apply_sandbox({})); + auto client = TRY(IPC::take_over_accepted_client_from_system_server(mach_server_name)); auto& heap = Web::Bindings::main_thread_vm().heap();