CVE-2026-12327
Overview
Background
- `WidgetEvent`
- Gecko’s base representation of an input/UI event, tagged by an
mClass(EventClassID) and anmMessage(EventMessage) that together identify its concrete subclass. - `EventClassID`
- A
uint8_tenum naming whichWidgetEventsubclass an event object actually is (e.g.eKeyboardEventClass,eMouseEventClass). - `EventMessage`
- An enum naming the specific event action (e.g.
eKeyDown,eMouseMove) that must be consistent with the object’sEventClassID. - `ParamTraits<mozilla::WidgetEvent>`
- The IPC serialization trait in
nsGUIEventIPC.hthat reads aWidgetEventsent from another (potentially compromised) process.
Root Cause Analysis
When a WidgetEvent was deserialized from another process, ParamTraits<mozilla::WidgetEvent>::Read validated only that the incoming aResult->mClass matched the aExpectedEventClassID for the deserialization site, but never checked that aResult->mMessage was a legal value for that class. The invariant that a WidgetEvent’s mMessage must correspond to its mClass (so that downstream code can safely reinterpret the object as the subclass implied by its message) was therefore not enforced on the trust boundary. A compromised content process could send an event carrying a valid mClass but an mMessage belonging to a different class, and code later dispatching on mMessage would treat the object as the wrong subclass, producing type confusion and memory corruption.
The fix adds IsValidMessageForIPC(aMessage, aClassID), a table mapping each EventMessage to its required EventClassID, and requires it to pass in addition to the class check; any mismatched or unrecognized mMessage (the default: return false; case) is now rejected. Rejected events have their mClass reset to eEventClassUninitialized and cause Read to fail, so the tampered event is never dispatched.
mMessage/mClass free to be inconsistent; the fix closes this by rejecting any event whose mMessage is not a valid message for its declared EventClassID.Attack Path
- Compromise a content process The attacker gains code execution in a sandboxed content process, giving them control over the bytes serialized into IPC messages.
- Forge a mismatched event
They craft a
WidgetEventwhosemClassequals the class expected at a deserialization site but whosemMessagebelongs to a different event class. - Pass the weak class check
In
ParamTraits<mozilla::WidgetEvent>::Read, the oldaResult->mClass == aExpectedEventClassIDtest succeeds because only the class was validated. - Trigger type confusion
Parent-process code later branches on
mMessage, reinterpreting the object as the subclass implied by the message and accessing fields that do not match the real object layout. - Corrupt memory The mismatched interpretation reads or writes memory out of bounds or through incorrect pointers, yielding memory corruption potentially leading to code execution.
Impact Assessment
WidgetEvent with an inconsistent mMessage/mClass pair over IPC. This is a sandbox-escape-class primitive: the precondition is a compromised content process, and the metadata rates it moderate memory-safety severity with presumed potential for arbitrary code execution.Changed Functions
| Function | Change | Notes |
|---|---|---|
switchwidget/WidgetEventImpl.cpp |
modified |
Files Changed
widget/EventForwards.hwidget/WidgetEventImpl.cppwidget/nsGUIEventIPC.h
Audit Directions
- Multi-field tag validationWhen deserializing tagged unions or polymorphic objects from IPC, verify that every discriminant field (both class and message here) is mutually consistent, not just one of them.
- Cross-process type confusionAudit sites that downcast or branch on a message/kind field of an IPC-received object for whether that field was validated against the object’s actual type on the receiving side.
- Default-deny enumsEnsure validators over enum values use a
default: return false;reject stance so newly added or out-of-range values cannot silently pass the trust boundary.
Patch
diff --git a/widget/EventForwards.h b/widget/EventForwards.h
index 82cba445004..907fccbf945 100644
--- a/widget/EventForwards.h
+++ b/widget/EventForwards.h
@@ -117,6 +117,15 @@ enum EventClassID : uint8_t {
const char* ToChar(EventClassID aEventClassID);
+/**
+ * Return true if aMessage is a valid EventMessage value for aClassID when an
+ * event is read from another process. This is used to reject events whose
+ * mMessage/mClass combination is inconsistent and therefore likely tampered
+ * with by a compromised content process.
+ */
+[[nodiscard]] bool IsValidMessageForIPC(EventMessage aMessage,
+ EventClassID aClassID);
+
typedef uint16_t Modifiers;
#define NS_DEFINE_KEYNAME(aCPPName, aDOMKeyName) KEY_NAME_INDEX_##aCPPName,
diff --git a/widget/WidgetEventImpl.cpp b/widget/WidgetEventImpl.cpp
index e5ee9b94d70..c4d3b451dd7 100644
--- a/widget/WidgetEventImpl.cpp
+++ b/widget/WidgetEventImpl.cpp
@@ -185,6 +185,75 @@ bool IsForbiddenDispatchingToNonElementContent(EventMessage aMessage) {
}
}
+bool IsValidMessageForIPC(EventMessage aMessage, EventClassID aClassID) {
+ switch (aMessage) {
+ case eKeyDown:
+ case eKeyUp:
+ case eKeyPress:
+ return aClassID == eKeyboardEventClass;
+ case eMouseMove:
+ case eMouseUp:
+ case eMouseDown:
+ case eMouseEnterIntoWidget:
+ case eMouseExitFromWidget:
+ case eMouseDoubleClick:
+ case eMouseActivate:
+ case eMouseOver:
+ case eMouseOut:
+ case eMouseHitTest:
+ case eMouseEnter:
+ case eMouseLeave:
+ case eMouseTouchDrag:
+ case eMouseLongTap:
+ case eMouseExploreByTouch:
+ return aClassID == eMouseEventClass;
+ case eWheel:
+ case eWheelOperationStart:
+ case eWheelOperationEnd:
+ return aClassID == eWheelEventClass;
+ case eDragEnter:
+ case eDragOver:
+ case eDragExit:
+ case eDrag:
+ case eDragEnd:
+ case eDragStart:
+ case eDrop:
+ case eDragLeave:
+ return aClassID == eDragEventClass;
+ case ePointerMove:
+ case ePointerUp:
+ case ePointerDown:
+ case ePointerOver:
+ case ePointerOut:
+ case ePointerEnter:
+ case ePointerLeave:
+ case ePointerCancel:
+ case ePointerRawUpdate:
+ case ePointerGotCapture:
+ case ePointerLostCapture:
+ case ePointerClick:
+ case ePointerAuxClick:
+ case eContextMenu:
+ return aClassID == ePointerEventClass;
+ case eTouchStart:
+ case eTouchMove:
+ case eTouchEnd:
+ case eTouchCancel:
+ case eTouchPointerCancel:
+ return aClassID == eTouchEventClass;
+ case eCompositionStart:
+ case eCompositionEnd:
+ case eCompositionChange:
+ case eCompositionCommitAsIs:
+ case eCompositionCommit:
+ return aClassID == eCompositionEventClass;
+ case eSetSelection:
+ return aClassID == eSelectionEventClass;
+ default:
+ return false;
+ }
+}
+
const char* ToChar(EventClassID aEventClassID) {
switch (aEventClassID) {
#define NS_ROOT_EVENT_CLASS(aPrefix, aName) \
diff --git a/widget/nsGUIEventIPC.h b/widget/nsGUIEventIPC.h
index f9f005abac0..baf960fdb59 100644
--- a/widget/nsGUIEventIPC.h
+++ b/widget/nsGUIEventIPC.h
@@ -83,13 +83,15 @@ struct ParamTraits<mozilla::WidgetEvent> {
ToChar(aExpectedEventClassID), ToChar(aResult->mClass),
ToChar(aResult->mMessage))
.c_str());
- if (aResult->mClass == aExpectedEventClassID) [[likely]] {
+ if (aResult->mClass == aExpectedEventClassID &&
+ mozilla::IsValidMessageForIPC(aResult->mMessage, aExpectedEventClassID))
+ [[likely]] {
return true;
}
// Clear mClass value to avoid the assertion failure in the destructor in
// the debug build because it's not a fault in this process.
aResult->mClass = mozilla::eEventClassUninitialized;
- // Don't allow illegal mClass value.
+ // Don't allow illegal mClass/mMessage combination.
return false;
}