CVE-2025-2857
Overview
Background
- `Channel::ChannelImpl::AcceptHandles`
- parent-side routine that, for an incoming message carrying handles, reads each
uint32_t handleValuefrom the payload, reconstructs aHANDLEviaUint32ToHandle, and duplicates it out of the sending (child) process into the current (parent) process withDUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE. - `Channel::ChannelImpl::TransferHandles`
- the sending side that duplicates a local handle into the peer process.
- Pseudo-handle
- a special Windows
HANDLEvalue that does not name a specific kernel object but is resolved relative to the calling process:GetCurrentProcess()returns(HANDLE)-1,GetCurrentThread()returns(HANDLE)-2, and (per the borrowed Chromium comment) fuzzing found problematic values down to-12. WhenDuplicateHandleis given a pseudo-handle as the source, it duplicates whatever that pseudo-handle means in the source process context. - `INVALID_HANDLE_VALUE`
- the value
(HANDLE)-1, which is numerically identical to theGetCurrentProcess()pseudo-handle.
Root Cause Analysis
In AcceptHandles, the parent validated each attacker-supplied handle value only with if (!ipc_handle || ipc_handle == INVALID_HANDLE_VALUE). That check rejects null and the single value -1, but it does not reject the other process pseudo-handles (-2 for the current thread, and other negative sentinels down to -12). The handle value comes from the message payload, i.e. it is fully controlled by the (possibly compromised) child process. When a malicious child placed a pseudo-handle value such as (HANDLE)-2 into a handle-bearing message, the parent passed it directly to ::DuplicateHandle(other_process_, ipc_handle, GetCurrentProcess(), ...). Because other_process_ is the child, DuplicateHandle interpreted the pseudo-handle relative to the child, but the duplication target is the parent (GetCurrentProcess()), and the resulting duplicated handle can refer to a far more powerful object than the IPC protocol intended — the “unintentionally powerful handle” the advisory describes, returned to and usable by the child. This is the same defect pattern as the in-the-wild Chrome sandbox escape CVE-2025-2783: trusting a child-supplied handle value that turns out to be a pseudo-handle.
The patch introduces an IsPseudoHandle(HANDLE) helper (borrowed from Chromium’s base/win/win_util.h) that returns true for handle values in [-12, 0), and a DuplicateRealHandle wrapper that asserts MOZ_RELEASE_ASSERT(!IsPseudoHandle(source_handle)) before calling ::DuplicateHandle. AcceptHandles now rejects an incoming handle with if (!ipc_handle || IsPseudoHandle(ipc_handle)) instead of the narrow == INVALID_HANDLE_VALUE test, so any pseudo-handle from the child is refused at the gate. Both the receive path (AcceptHandles) and the send path (TransferHandles) are switched from raw ::DuplicateHandle to DuplicateRealHandle, so the no-pseudo-handle invariant is enforced (and release-asserted) on every handle duplication across the IPC boundary. The advisory’s SandboxEscape / CWE-668 (“exposure to wrong sphere”) classification is exactly what the diff substantiates: a compromised child steers the parent into duplicating a privileged handle into the wrong sphere (back to the child).
-1, so a compromised child could send a Windows pseudo-handle (e.g. GetCurrentThread() = -2) and make the parent hand back an unintentionally powerful handle — the fix rejects the whole pseudo-handle range [-12, 0) on every duplication.Attack Path
- Compromised child An attacker who already has code execution in a sandboxed Firefox child process (e.g. a renderer exploited via a content bug) crafts an IPC message that carries a transferred handle.
- Smuggle a pseudo-handle
Instead of a real object handle, the child places a pseudo-handle value such as
(HANDLE)-2(GetCurrentThread) — or another sentinel in[-12, -1]not equal toINVALID_HANDLE_VALUE— into the serialized handle payload. - Bypass the weak check
The parent’s
AcceptHandlesold guard (== INVALID_HANDLE_VALUE) does not catch these values, so it proceeds to::DuplicateHandle(other_process_ /*child*/, ipc_handle, GetCurrentProcess() /*parent*/, ...). - Parent duplicates a powerful handle
The pseudo-handle resolves in the child’s context to a privileged object (e.g. a thread/process handle) and a usable duplicate is produced, with
DUPLICATE_CLOSE_SOURCEsemantics, returnable to the child. - Primitive The child obtains a handle to a resource it should never have, escalating out of the sandbox into parent-level capability (sandbox escape).
Impact Assessment
S:C): the child induces the privileged parent to create and expose an unintentionally powerful handle, which is a classic stepping stone to full parent-process control and thus escape from the content sandbox. The diff concretely substantiates the handle-validation gap and its fix; the exact privileged object obtained and the subsequent escalation are implementation details not enumerated by the diff, but the pattern (and its CVE-2025-2783 analogue, exploited in the wild in Chrome) makes the sandbox-escape consequence well founded. No user interaction is required (UI:N).Changed Functions
| Function | Change | Notes |
|---|---|---|
foripc/chromium/src/chrome/common/ipc_channel_win.cc |
modified | |
ifipc/chromium/src/chrome/common/ipc_channel_win.cc |
modified |
Files Changed
ipc/chromium/src/chrome/common/ipc_channel_win.cc
Audit Directions
- Every `::DuplicateHandle` call sitegrep the Windows IPC/child-management code for remaining raw
::DuplicateHandleuses and confirm each either goes throughDuplicateRealHandleor has a documented, legitimate pseudo-handle use. - Other child-supplied OS resource identifiersreview any IPC path where the parent acts on a value controlled by a child (handle values, PIDs, section/mapping names, section offsets) with validation that only blocks a couple of sentinels rather than enforcing a positive-allowlist of expected values.
- Non-Windows handle transfer analoguesalthough this CVE is Windows-only, check the file-descriptor transfer logic on other platforms for conceptually similar “trust the child’s descriptor without classifying it” assumptions.
- CVE-2025-2783 pattern sweepmore broadly, audit broker/parent code that duplicates handles on behalf of less-privileged processes for the specific pseudo-handle-as-source footgun that both this bug and the Chrome original share.
Patch
diff --git a/ipc/chromium/src/chrome/common/ipc_channel_win.cc b/ipc/chromium/src/chrome/common/ipc_channel_win.cc
index 316776b8ac1..7aca70046df 100644
--- a/ipc/chromium/src/chrome/common/ipc_channel_win.cc
+++ b/ipc/chromium/src/chrome/common/ipc_channel_win.cc
@@ -29,6 +29,34 @@
using namespace mozilla::ipc;
+namespace {
+
+// This logic is borrowed from Chromium's `base/win/win_util.h`. It allows us
+// to distinguish pseudo-handle values, such as returned by GetCurrentProcess()
+// (-1), GetCurrentThread() (-2), and potentially more. The code there claims
+// that fuzzers have found issues up until -12 with DuplicateHandle.
+//
+// https://source.chromium.org/chromium/chromium/src/+/36dbbf38697dd1e23ef8944bb9e57f6e0b3d41ec:base/win/win_util.h
+inline bool IsPseudoHandle(HANDLE handle) {
+ auto handleValue = static_cast<int32_t>(reinterpret_cast<uintptr_t>(handle));
+ return -12 <= handleValue && handleValue < 0;
+}
+
+// A real handle is a handle that is not a pseudo-handle. Always preferably use
+// this variant over ::DuplicateHandle. Only use stock ::DuplicateHandle if you
+// explicitly need the ability to duplicate a pseudo-handle.
+inline bool DuplicateRealHandle(HANDLE source_process, HANDLE source_handle,
+ HANDLE target_process, LPHANDLE target_handle,
+ DWORD desired_access, BOOL inherit_handle,
+ DWORD options) {
+ MOZ_RELEASE_ASSERT(!IsPseudoHandle(source_handle));
+ return static_cast<bool>(::DuplicateHandle(
+ source_process, source_handle, target_process, target_handle,
+ desired_access, inherit_handle, options));
+}
+
+} // namespace
+
namespace IPC {
//------------------------------------------------------------------------------
@@ -594,7 +622,7 @@ bool Channel::ChannelImpl::AcceptHandles(Message& msg) {
nsTArray<mozilla::UniqueFileHandle> handles(num_handles);
for (uint32_t handleValue : payload) {
HANDLE ipc_handle = Uint32ToHandle(handleValue);
- if (!ipc_handle || ipc_handle == INVALID_HANDLE_VALUE) {
+ if (!ipc_handle || IsPseudoHandle(ipc_handle)) {
CHROMIUM_LOG(ERROR)
<< "Attempt to accept invalid or null handle from process "
<< other_pid_ << " for message " << msg.name() << " in AcceptHandles";
@@ -612,9 +640,10 @@ bool Channel::ChannelImpl::AcceptHandles(Message& msg) {
CHROMIUM_LOG(ERROR) << "other_process_ is invalid in AcceptHandles";
return false;
}
- if (!::DuplicateHandle(other_process_, ipc_handle, GetCurrentProcess(),
- getter_Transfers(local_handle), 0, FALSE,
- DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE)) {
+ if (!DuplicateRealHandle(
+ other_process_, ipc_handle, GetCurrentProcess(),
+ getter_Transfers(local_handle), 0, FALSE,
+ DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE)) {
DWORD err = GetLastError();
// Don't log out a scary looking error if this failed due to the target
// process terminating.
@@ -687,9 +716,9 @@ bool Channel::ChannelImpl::TransferHandles(Message& msg) {
CHROMIUM_LOG(ERROR) << "other_process_ is invalid in TransferHandles";
return false;
}
- if (!::DuplicateHandle(GetCurrentProcess(), local_handle.get(),
- other_process_, &ipc_handle, 0, FALSE,
- DUPLICATE_SAME_ACCESS)) {
+ if (!DuplicateRealHandle(GetCurrentProcess(), local_handle.get(),
+ other_process_, &ipc_handle, 0, FALSE,
+ DUPLICATE_SAME_ACCESS)) {
DWORD err = GetLastError();
// Don't log out a scary looking error if this failed due to the target
// process terminating.