# Security Implications of Ladybird's Multi-Process Architecture: Process Isolation Explained

> Explore Ladybird's multi-process architecture and its security benefits. Learn how process isolation protects your browser UI and system resources from untrusted web content.

- Repository: [Ladybird/ladybird](https://github.com/LadybirdBrowser/ladybird)
- Tags: security
- Published: 2026-03-05

---

**Ladybird's multi-process architecture isolates untrusted web content into separate OS processes, ensuring that memory corruption in a renderer cannot compromise the browser UI or access privileged system resources.**

LadybirdBrowser/ladybird implements a rigorous multi-process security model that divides browser responsibilities across distinct operating-system processes. This architecture ensures that JavaScript execution, HTML rendering, and network operations run in sandboxed environments separate from the privileged UI process. Understanding the security implications of Ladybird's multi-process architecture reveals how the browser mitigates memory safety vulnerabilities and contains potential exploits.

## Core Process Types and Security Boundaries

Ladybird segregates functionality into specialized process types, each with strictly defined privileges. This separation is orchestrated by the **WebView** library and enforced by the operating system's address space isolation.

- **Browser process**: Executes the UI code, manages user preferences, and houses the privileged `WebView::ProcessManager`. It retains full file system access but never executes untrusted web code directly.
- **WebContent processes**: Handle individual web pages, execute JavaScript, and enforce HTML sandboxing rules. Each page runs in its own process, preventing memory-corruption exploits from affecting the UI or other tabs.
- **WebWorker processes**: Execute background scripts with the same isolation guarantees as WebContent processes. Workers cannot access the UI or other workers without explicit IPC.
- **RequestServer and ImageDecoder processes**: Handle network requests and image decoding in sandboxed helpers. These limit the attack surface of vulnerable codecs or network stacks, ensuring crashes remain contained.

## Process Lifecycle Management and Signal Handling

The `ProcessManager` class in [`Libraries/LibWebView/ProcessManager.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWebView/ProcessManager.cpp) centralizes process tracking and safe termination handling. When constructed, it registers a `SIGCHLD` handler to reap zombie processes and forward sanitized exit notifications to the UI.

```cpp
// ProcessManager.cpp, line 55-66
m_signal_handle = Core::EventLoop::register_signal(SIGCHLD, [this](int) {
    auto result = Core::System::waitpid(-1, WNOHANG);
    while (!result.is_error() && result.value().pid > 0) {
        …
        if (auto process = remove_process(pid); process.has_value()) {
            Core::deferred_invoke([this, process = process.release_value()]() mutable {
                on_process_exited(move(process));
            });
        }
    }
});

```

This centralized bookkeeping ensures that the UI only receives sanitized statistics through `ProcessManager::serialize_json`, preventing use-after-free bugs when querying process statistics. The deferred invocation mechanism guarantees that `on_process_exited` callbacks execute safely without blocking the signal handler.

## Privilege Separation Mechanisms

Child processes are spawned via `Process::spawn_and_connect_to_process` defined in [`Libraries/LibWebView/Process.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWebView/Process.cpp). The implementation enforces **least-privilege** principles by transferring only the necessary socket descriptor via the `SOCKET_TAKEOVER` environment variable.

```cpp
// Process.cpp, line 50-53 – pass socket fd to child
auto takeover_string = MUST(String::formatted("{}:{}", options.name, socket_fds[1]));
TRY(Core::Environment::set("SOCKET_TAKEOVER"sv, takeover_string, Core::Environment::Overwrite::Yes));

```

All other file descriptors are closed or marked with `FD_CLOEXEC` before the fork. When optional stdout/stderr capture is requested, pipes are created with `O_CLOEXEC` cleared only on the write ends, then the parent immediately closes its copies after fork:

```cpp
// Process.cpp, line 60-73 – create pipes only if requested
if (capture_output) {
    stdout_pipe = TRY(Core::System::pipe2(O_CLOEXEC));
    …
    spawn_options.file_actions.append(Core::FileAction::DupFd { .write_fd = stdout_pipe[1], .fd = STDOUT_FILENO });
    …
}

```

This prevents accidental file descriptor leakage, ensuring that compromised child processes cannot inherit sensitive handles from the privileged browser process.

## Platform-Specific Hardening

On macOS, Ladybird leverages Mach ports for secure statistics querying. The initial browser process registers its send port during `ProcessManager` construction:

```cpp
// ProcessManager.cpp, line 75-80 – expose Mach port for the current pid
auto self_send_port = mach_task_self();
…
set_process_mach_port(getpid(), Core::MachPort::adopt_right(self_send_port, Core::MachPort::PortRight::Send));

```

This allows the UI to query per-process statistics without requiring additional privileges. On Windows, the codebase utilizes `SetHandleInformation` to clear inheritance flags, while Linux relies on explicit `close_on_exec` flags to prevent handle leakage across privilege boundaries.

## Containment of Untrusted Code

The multi-process architecture interacts with HTML sandboxing policies defined in [`Libraries/LibWeb/HTML/HTMLIFrameElement.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWeb/HTML/HTMLIFrameElement.cpp) and [`Libraries/LibWeb/HTML/Navigable.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWeb/HTML/Navigable.cpp). When a sandboxed iframe is created, it runs in its own WebContent process, enforcing **"no-script"** or **"no-top-navigation"** rules both at the JavaScript engine level and through OS-level process isolation.

The `Navigable::allowed_by_sandboxing_to_navigate` function checks navigation permissions before allowing cross-origin requests. Because each sandboxed context runs in a separate process with its own address space, an attacker exploiting a rendering engine bug cannot directly tamper with the browser's internal state or other tabs.

## Summary

- **Process isolation** confines WebContent and WebWorker processes to separate address spaces, preventing memory corruption from spreading to the UI or other tabs.
- **Privilege separation** ensures only the Browser process accesses user files and system resources, while renderers operate with minimal permissions.
- **Secure IPC** relies exclusively on socketpairs created via `SOCKET_TAKEOVER`, eliminating file descriptor leakage vectors.
- **Signal safety** through centralized `SIGCHLD` handling prevents zombie processes and use-after-free vulnerabilities in process statistics.
- **Platform hardening** via Mach ports (macOS) and `FD_CLOEXEC` flags (Linux/Windows) further restricts cross-process handle inheritance.

## Frequently Asked Questions

### How does Ladybird prevent a crashed renderer from affecting the browser UI?

When a WebContent process crashes, the kernel delivers a `SIGCHLD` signal to the Browser process. The `ProcessManager` registered in [`Libraries/LibWebView/ProcessManager.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWebView/ProcessManager.cpp) immediately removes the dead process from its internal map via `remove_process()` before the UI queries statistics. This ensures the UI never dereferences stale process objects, and because each renderer runs in a separate OS process, a crash cannot corrupt the Browser process's memory space.

### What mechanism ensures file descriptors aren't leaked to child processes?

The `spawn_and_connect_to_process` function in [`Libraries/LibWebView/Process.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWebView/Process.cpp) explicitly marks all file descriptors with `FD_CLOEXEC` (or `O_CLOEXEC` for pipes) before forking. Only the single socket descriptor required for IPC is passed via the `SOCKET_TAKEOVER` environment variable. On Windows, the equivalent `SetHandleInformation` calls ensure handles are non-inheritable, preventing privilege escalation attacks that rely on handle leakage.

### How does process isolation interact with HTML sandbox attributes?

Ladybird enforces HTML sandboxing flags (such as `sandbox="allow-scripts"`) at two levels. First, the `HTMLIFrameElement` parsing logic in [`Libraries/LibWeb/HTML/HTMLIFrameElement.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWeb/HTML/HTMLIFrameElement.cpp) extracts these flags. Second, `Navigable::allowed_by_sandboxing_to_navigate` in [`Libraries/LibWeb/HTML/Navigable.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWeb/HTML/Navigable.cpp) validates navigation requests against these restrictions. Crucially, sandboxed iframes run in distinct WebContent processes, so even if an attacker bypasses the JavaScript-level checks, the OS process boundary prevents access to the parent frame's resources.

### What happens when a WebContent process exits unexpectedly?

The `ProcessManager` invokes the `on_process_exited` callback after safely reaping the process via `waitpid(-1, WNOHANG)`. This deferred invocation ensures the UI receives a clean `WebView::Process` object representing the terminated instance, allowing the interface to update its process list and release associated resources without risking race conditions or use-after-free errors.