CVE-2025-3032
Overview
Background
- `MiniTransceiver`
- The fork server’s IPC channel object; it communicates over a single IPC file descriptor.
- `UniqueFileHandle` / `ipcHandle`
- RAII owner of a Unix FD.
release()relinquishes ownership (returns the raw fd and stops tracking it);extract()moves ownership out.
Root Cause Analysis
Extra file descriptors were leaking into the fork server and therefore into the content processes it forks, and the fork server’s own IPC FD was not reliably owned for the lifetime of its transceiver. In ForkServer::ForkServer, the channel was built with MakeUnique<MiniTransceiver>(ipcHandle->release(), ...): release() hands the raw fd to the transceiver but drops RAII ownership, so the FD’s lifetime is untracked (and nothing guarantees it is held only as long as the transceiver, nor closed correctly). More importantly, the launcher attached process-wide FDs to the fork server that a fork server has no business holding: the crash reporter notification pipe (CrashReporter::GetChildNotificationPipe() via sCrashReporter) was added whenever crash reporting was enabled, and AttachSandboxReporter added a sandbox-reporter FD. Because the fork server passes its inherited FDs down to every forked content process, those privileged descriptors leaked across the process boundary into sandboxed web-content processes, where they should not exist. The advisory frames this as “leaking of file descriptors from the fork server to web content processes [that] could allow for privilege escalation” (CWE-403, uncontrolled resource/FD leak).
The patch closes each leak. ForkServer now keeps the IPC FD in a new owned member UniqueFileHandle mIpcFd = ipcHandle.extract() and constructs the transceiver from mIpcFd.get(), so the descriptor is held exactly as long as the transceiver and managed via RAII. BaseProcessLauncher::DoSetup no longer adds the crash-reporter pipe when mProcessType == GeckoProcessType_ForkServer. PosixProcessLauncher::DoSetup asserts that a fork-server launch carries exactly one FD argument (mChildArgs.mFiles.size() == 1), i.e. only its IPC channel. SandboxLaunch::Configure skips AttachSandboxReporter for the fork server. Together these ensure the fork server inherits only its single IPC FD, so no privileged descriptor is propagated to forked content processes. The advisory’s “LogicError” label understates it: the diff is a concrete FD-leak / resource-management fix with a (inferred) privilege-escalation consequence.
Attack Path
- Compromised renderer An attacker gains code execution in a sandboxed content process (precondition; the FD leak is a post-compromise escalation aid, inferred).
- Inherited FD That content process was forked by the fork server and inherited the leaked privileged descriptor(s) — e.g. the crash-reporter notification pipe or sandbox-reporter FD.
- FD abuse The attacker operates on the inherited descriptor from inside the sandbox to reach functionality or a process the sandbox was meant to deny (inferred; the diff proves the leak, not the specific abuse).
- Privilege escalation Using the unexpected channel/resource, the attacker influences a more-privileged component, weakening or escaping the content sandbox.
- Primitive A sandbox-escape / privilege-escalation stepping stone granted by descriptors that should never have crossed into content processes.
Impact Assessment
MOZ_ENABLE_FORKSERVER and crash reporting/sandboxing enabled. CVSS C:H/I:H/A:N, AC:H reflects the post-compromise, condition-dependent nature.Files Changed
ipc/glue/ForkServer.cppipc/glue/ForkServer.hipc/glue/GeckoChildProcessHost.cppsecurity/sandbox/linux/launch/SandboxLaunch.cpp
Audit Directions
- Other FDs reaching the fork serverAudit all
mChildArgs.mFiles/fds_to_remapcontributors (logging, profiler, GPU, IPC relays) for descriptors added unconditionally that would be inherited by forked children; the new single-FD assertion is the invariant to uphold. - `release()` vs `extract()` ownershipGrep for
UniqueFileHandle/UniquePtr->release()feeding long-lived objects; untracked raw FDs/pointers are both leak and double-close hazards. - Fork inheritance hygieneReview what the fork server leaves open across
fork()(CLOEXEC flags, explicit closes) to ensure no additional inherited resources (sockets, shared memory handles) cross into sandboxed children.
Patch
diff --git a/ipc/glue/ForkServer.cpp b/ipc/glue/ForkServer.cpp
index 7b846b5d807..ba368de1575 100644
--- a/ipc/glue/ForkServer.cpp
+++ b/ipc/glue/ForkServer.cpp
@@ -47,7 +47,9 @@ ForkServer::ForkServer(int* aArgc, char*** aArgv) : mArgc(aArgc), mArgv(aArgv) {
MOZ_CRASH("forkserver missing ipcHandle argument");
}
- mTcver = MakeUnique<MiniTransceiver>(ipcHandle->release(),
+ // Hold our IPC FD while our MiniTransceiver is alive.
+ mIpcFd = ipcHandle.extract();
+ mTcver = MakeUnique<MiniTransceiver>(mIpcFd.get(),
DataBufferClear::AfterReceiving);
}
diff --git a/ipc/glue/ForkServer.h b/ipc/glue/ForkServer.h
index eb04a2089d5..51fb473e922 100644
--- a/ipc/glue/ForkServer.h
+++ b/ipc/glue/ForkServer.h
@@ -27,6 +27,7 @@ class ForkServer {
static bool RunForkServer(int* aArgc, char*** aArgv);
private:
+ UniqueFileHandle mIpcFd;
UniquePtr<MiniTransceiver> mTcver;
int* mArgc;
diff --git a/ipc/glue/GeckoChildProcessHost.cpp b/ipc/glue/GeckoChildProcessHost.cpp
index b0c74483ac4..755f90a4594 100644
--- a/ipc/glue/GeckoChildProcessHost.cpp
+++ b/ipc/glue/GeckoChildProcessHost.cpp
@@ -1120,7 +1120,8 @@ Result<Ok, LaunchError> BaseProcessLauncher::DoSetup() {
geckoargs::sParentPid.Put(static_cast<uint64_t>(base::GetCurrentProcId()),
mChildArgs);
- if (!CrashReporter::IsDummy() && CrashReporter::GetEnabled()) {
+ if (!CrashReporter::IsDummy() && CrashReporter::GetEnabled() &&
+ mProcessType != GeckoProcessType_ForkServer) {
#if defined(MOZ_WIDGET_COCOA) || defined(XP_WIN)
geckoargs::sCrashReporter.Put(CrashReporter::GetChildNotificationPipe(),
mChildArgs);
@@ -1310,6 +1311,12 @@ Result<Ok, LaunchError> PosixProcessLauncher::DoSetup() {
mChildArgs.mArgs.push_back(ChildProcessType());
+# ifdef MOZ_ENABLE_FORKSERVER
+ MOZ_ASSERT(mProcessType != GeckoProcessType_ForkServer ||
+ mChildArgs.mFiles.size() == 1,
+ "The ForkServer only expects a single FD argument");
+# endif
+
# if !defined(MOZ_WIDGET_ANDROID)
// Add any files which need to be transferred to fds_to_remap.
// NOTE: This doesn't transfer ownership of the files out of `mChildArgs`.
diff --git a/security/sandbox/linux/launch/SandboxLaunch.cpp b/security/sandbox/linux/launch/SandboxLaunch.cpp
index 0c916941158..0c2f62b5d59 100644
--- a/security/sandbox/linux/launch/SandboxLaunch.cpp
+++ b/security/sandbox/linux/launch/SandboxLaunch.cpp
@@ -306,7 +306,8 @@ bool SandboxLaunch::Configure(GeckoProcessType aType, SandboxingKind aKind,
// At this point, we know we'll be using sandboxing; generic
// sandboxing support goes here.
PreloadSandboxLib(&aOptions->env_map);
- if (!AttachSandboxReporter(aExtraOpts)) {
+ if (aType != GeckoProcessType_ForkServer &&
+ !AttachSandboxReporter(aExtraOpts)) {
return false;
}