High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionSandbox escape in the Security: Process Sandboxing component
ComponentGraphics
Bug ClassLogic Error
Tracker2058017
Fix commit95fab400ff8b (firefox) +137/-3
CISA KEVNot listed
CreditedMozilla
Disclosed2026-09-29

Changed Functions

FunctionChangeNotes
if
mozglue/misc/WindowsUserHandleValidation.h
modified
CompositorWindowThread
widget/windows/WinCompositorWindowThread.cpp
modified

Files Changed

  • gfx/layers/ipc/CompositorThread.cpp
  • gfx/webrender_bindings/RenderThread.cpp
  • modules/libpref/init/StaticPrefList.yaml
  • mozglue/misc/WindowsUserHandleValidation.h
  • mozglue/misc/moz.build
  • security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
  • widget/windows/WinCompositorWindowThread.cpp
diff --git a/gfx/layers/ipc/CompositorThread.cpp b/gfx/layers/ipc/CompositorThread.cpp
index caa63113121..6501b51b820 100644
--- a/gfx/layers/ipc/CompositorThread.cpp
+++ b/gfx/layers/ipc/CompositorThread.cpp
@@ -16,6 +16,9 @@
 #include "mozilla/media/MediaSystemResourceService.h"
 #include "nsThread.h"
 #include "nsThreadUtils.h"
+#ifdef XP_WIN
+#  include "mozilla/WindowsUserHandleValidation.h"
+#endif
 
 namespace mozilla {
 namespace layers {
@@ -94,6 +97,9 @@ CompositorThreadHolder::CreateCompositorThread() {
                 2048);
             nsCOMPtr<nsIThread> thread = NS_GetCurrentThread();
             static_cast<nsThread*>(thread.get())->SetUseHangMonitor(true);
+#ifdef XP_WIN
+            mozilla::ForceToGuiThreadAndFixTebValidateHandlesFlag();
+#endif
           }),
       {.stackSize = stackSize});
 
diff --git a/gfx/webrender_bindings/RenderThread.cpp b/gfx/webrender_bindings/RenderThread.cpp
index f2614ca13da..ffc29f2c327 100644
--- a/gfx/webrender_bindings/RenderThread.cpp
+++ b/gfx/webrender_bindings/RenderThread.cpp
@@ -37,6 +37,7 @@
 #ifdef XP_WIN
 #  include "GLContextEGL.h"
 #  include "GLLibraryEGL.h"
+#  include "mozilla/WindowsUserHandleValidation.h"
 #  include "mozilla/gfx/DeviceManagerDx.h"
 #  include "mozilla/webrender/DCLayerTree.h"
 #  include "mozilla/widget/WinCompositorWindowThread.h"
@@ -197,6 +198,9 @@ void RenderThread::Start(uint32_t aNamespace) {
             nsThread* nsthread = static_cast<nsThread*>(thread.get());
             nsthread->SetUseHangMonitor(true);
             nsthread->SetPriority(nsISupportsPriority::PRIORITY_HIGH);
+#ifdef XP_WIN
+            mozilla::ForceToGuiThreadAndFixTebValidateHandlesFlag();
+#endif
           }),
       {.stackSize = stackSize});
 
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
index 7dc5cdd1912..17a37337fc3 100644
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -18635,6 +18635,12 @@
     value: true
     mirror: always
 
+# If enabled, excepts all JOB_OBJECT_UILIMIT_* flags from the GPU process's job.
+-   name: security.sandbox.gpu.disable-job-uilimits
+    type: RelaxedAtomicBool
+    value: false
+    mirror: always
+
 -   name: security.sandbox.gmp.shadow-stack.enabled
     type: RelaxedAtomicBool
     value: true
diff --git a/mozglue/misc/WindowsUserHandleValidation.h b/mozglue/misc/WindowsUserHandleValidation.h
new file mode 100644
index 00000000000..9ca421b15f5
--- /dev/null
+++ b/mozglue/misc/WindowsUserHandleValidation.h
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZGLUE_MISC_WINDOWSUSERHANDLEVALIDATION_H_
+#define MOZGLUE_MISC_WINDOWSUSERHANDLEVALIDATION_H_
+
+#include <cstdint>
+
+#include <windows.h>
+#include <winternl.h>
+
+#include "mozilla/Assertions.h"
+
+namespace mozilla {
+
+// Bit in Win32ClientInfo user side flags that gates whether user32 APIs like
+// GetClientRect/ValidateHwnd/IsWindow route through the kernel's full
+// handle-validation path (NtUserValidateHandleSecure).
+static const uint32_t kValidateHandlesFlag = 0x20000000;
+
+inline uint32_t& GetWin32UserFlagsFromTeb() {
+  static const size_t kX64AndWowWin32ClientInfoOffsetInBytes = 0x800;
+  static const size_t kX64AndWowUserFlagsOffsetInBytes = 0x1c;
+
+  auto* teb = reinterpret_cast<uint8_t*>(NtCurrentTeb());
+  uint8_t* baseForOffset = teb;
+  size_t win32kClientInfoUserFlagsOffset;
+#if defined(_WIN64)
+  win32kClientInfoUserFlagsOffset =
+      kX64AndWowWin32ClientInfoOffsetInBytes + kX64AndWowUserFlagsOffsetInBytes;
+#else
+  static const size_t kNativeX86Win32ClientInfoOffsetInBytes = 0x6cc;
+  static const size_t kNativeX86UserFlagsOffsetInBytes = 0x14;
+
+  BOOL isWow64Process;
+  if (!::IsWow64Process(::GetCurrentProcess(), &isWow64Process)) {
+    MOZ_CRASH("IsWow64Process failed");
+  }
+
+  if (isWow64Process) {
+    // On win10+, x86 WOW user32 reads a signed delta at TEB+0xfdc and, if
+    // negative, adds it to the base TEB pointer before applying the offset.
+    int32_t delta = *reinterpret_cast<int32_t*>(teb + 0xfdc);
+    if (delta < 0) {
+      baseForOffset += delta;
+    }
+
+    win32kClientInfoUserFlagsOffset = kX64AndWowWin32ClientInfoOffsetInBytes +
+                                      kX64AndWowUserFlagsOffsetInBytes;
+  } else {
+    win32kClientInfoUserFlagsOffset = kNativeX86Win32ClientInfoOffsetInBytes +
+                                      kNativeX86UserFlagsOffsetInBytes;
+  }
+#endif
+
+  return *reinterpret_cast<uint32_t*>(baseForOffset +
+                                      win32kClientInfoUserFlagsOffset);
+}
+
+inline bool JobUILimitsRequireTebFlagClear() {
+  // Store result as job is not changed during process lifetime.
+  static const bool sClearRequired = [] {
+    JOBOBJECT_BASIC_UI_RESTRICTIONS uiRestrictionsInfo{};
+    if (!::QueryInformationJobObject(nullptr, JobObjectBasicUIRestrictions,
+                                     &uiRestrictionsInfo,
+                                     sizeof(uiRestrictionsInfo), nullptr)) {
+      // Not in a job or query failed;
+      return false;
+    }
+
+    // On older Windows the flag gets set incorrectly if any UI restrictions are
+    // set and it should only be set if JOB_OBJECT_UILIMIT_HANDLES is set.
+    // So, the flag needs clearing if there are restrictions and
+    // JOB_OBJECT_UILIMIT_HANDLES is not set.
+    return uiRestrictionsInfo.UIRestrictionsClass &&
+           !(uiRestrictionsInfo.UIRestrictionsClass &
+             JOB_OBJECT_UILIMIT_HANDLES);
+  }();
+  return sClearRequired;
+}
+
+inline void ForceToGuiThreadAndFixTebValidateHandlesFlag() {
+  if (!JobUILimitsRequireTebFlagClear()) {
+    return;
+  }
+
+  BOOL isGuiThread = ::IsGUIThread(/* bConvertToGuiThread */ TRUE);
+  if (!isGuiThread) {
+    MOZ_CRASH("Failed to convert to GUI Thread.");
+  }
+
+  auto& userFlags = GetWin32UserFlagsFromTeb();
+  userFlags &= ~kValidateHandlesFlag;
+}
+
+}  // namespace mozilla
+
+#endif  // MOZGLUE_MISC_WINDOWSUSERHANDLEVALIDATION_H_
diff --git a/mozglue/misc/moz.build b/mozglue/misc/moz.build
index b92ab85f866..fd16921f3eb 100644
--- a/mozglue/misc/moz.build
+++ b/mozglue/misc/moz.build
@@ -84,6 +84,7 @@ if CONFIG["OS_ARCH"] == "WINNT":
         "WindowsProcessMitigations.h",
         "WindowsStackCookie.h",
         "WindowsUnwindInfo.h",
+        "WindowsUserHandleValidation.h",
     ]
     EXPORTS.mozilla.glue += [
         "WindowsUnicode.h",
diff --git a/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp b/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
index eff029e9a63..74c0e5486b8 100644
--- a/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
+++ b/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
@@ -1344,11 +1344,15 @@ void SandboxBroker::SetSecurityLevelForGPUProcess(int32_t aSandboxLevel) {
   sandbox::IntegrityLevel initialIntegrityLevel = sandbox::INTEGRITY_LEVEL_LOW;
   sandbox::IntegrityLevel delayedIntegrityLevel = sandbox::INTEGRITY_LEVEL_LOW;
 
-  sandbox::JobLevel jobLevel = sandbox::JobLevel::kLimitedUser;
+  sandbox::JobLevel jobLevel = sandbox::JobLevel::kLockdown;
 
   uint32_t uiExceptions =
       JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS | JOB_OBJECT_UILIMIT_DESKTOP |
-      JOB_OBJECT_UILIMIT_EXITWINDOWS | JOB_OBJECT_UILIMIT_DISPLAYSETTINGS;
+      JOB_OBJECT_UILIMIT_EXITWINDOWS | JOB_OBJECT_UILIMIT_DISPLAYSETTINGS |
+      JOB_OBJECT_UILIMIT_HANDLES | JOB_OBJECT_UILIMIT_GLOBALATOMS;
+  if (StaticPrefs::security_sandbox_gpu_disable_job_uilimits()) {
+    uiExceptions |= JOB_OBJECT_UILIMIT_ALL;
+  }
 
   sandbox::MitigationFlags initialMitigations =
       sandbox::MITIGATION_BOTTOM_UP_ASLR | sandbox::MITIGATION_HEAP_TERMINATE |
diff --git a/widget/windows/WinCompositorWindowThread.cpp b/widget/windows/WinCompositorWindowThread.cpp
index 0b89e16d45c..10f5d13b6f1 100644
--- a/widget/windows/WinCompositorWindowThread.cpp
+++ b/widget/windows/WinCompositorWindowThread.cpp
@@ -8,6 +8,7 @@
 #include "mozilla/StaticMonitor.h"
 #include "mozilla/StaticPrefs_apz.h"
 #include "mozilla/StaticPtr.h"
+#include "mozilla/WindowsUserHandleValidation.h"
 #include "mozilla/gfx/Logging.h"
 #include "mozilla/layers/SynchronousTask.h"
 #include "transport/runnable_utils.h"
@@ -20,6 +21,19 @@ static StaticRefPtr<WinCompositorWindowThread> sWinCompositorWindowThread;
 static StaticMonitor sShutdownMonitor;
 static bool sShutdownComplete MOZ_GUARDED_BY(sShutdownMonitor) = false;
 
+// Ensures the flag that forces user32 handle validation calls onto the slow
+// kernel path is cleared for this thread, since it creates, destroys and
+// dispatches messages to its own HWNDs. Overriding Init() (rather than
+// posting a task after Start()) guarantees this runs on every OS thread this
+// object drives, including after a Stop()/StartWithOptions() restart.
+class CompositorWindowThread final : public base::Thread {
+ public:
+  CompositorWindowThread() : base::Thread("WinCompositor") {}
+
+ protected:
+  void Init() override { ForceToGuiThreadAndFixTebValidateHandlesFlag(); }
+};
+
 /// A window procedure that logs when an input event is received to the gfx
 /// error log
 ///
@@ -90,7 +104,7 @@ void WinCompositorWindowThread::Start() {
     sWinCompositorWindowThread = nullptr;
   }
 
-  base::Thread* thread = new base::Thread("WinCompositor");
+  base::Thread* thread = new CompositorWindowThread();
   if (!thread->StartWithOptions(options)) {
     delete thread;
     return;
Loading diff…