commit e4cdb388d44d6438687cea366201f16510659ff2
parent 9e491f2a6fefcdeff634c3c9b6cdda127d836e3d
Author: David P. <daparks@mozilla.com>
Date: Thu, 23 Oct 2025 22:07:12 +0000
Bug 1990634: Add WindowsTestDebug component to find open file handles r=win-reviewers,gstoll
The component is (only) useful for debugging when Windows tells us a process
has an open file handle and forbids operations on that file. The utility
will return a list of applications with a handle to the file. This was written
to debug this bug, but this bug was worse than it could handle -- file handles
can be cloned for memory mapping and it seems that the Restart Manager (the
Win32 tool we use to implement this) does not catch those file handles.
Process Explorer can do this as well. (Pro tip: it even shows the memory
mapping handles... in the DLLs tab!). This component is most useful for
fast, transient issues (such as Windows Defender grabbing a handle at an
inopporitune time), or for times when interactivity isn't available (such
as failures in automation, like this bug).
Differential Revision: https://phabricator.services.mozilla.com/D267710
Diffstat:
8 files changed, 316 insertions(+), 0 deletions(-)
diff --git a/widget/windows/WindowsTestDebug.cpp b/widget/windows/WindowsTestDebug.cpp
@@ -0,0 +1,146 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "WindowsTestDebug.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsTArray.h"
+#include "nsXPCOM.h"
+
+namespace {
+typedef DWORD(WINAPI* RMSTARTSESSION)(DWORD*, DWORD, WCHAR[]);
+typedef DWORD(WINAPI* RMREGISTERRESOURCES)(DWORD, UINT, LPCWSTR[], UINT,
+ RM_UNIQUE_PROCESS[], UINT,
+ LPCWSTR[]);
+typedef DWORD(WINAPI* RMGETLIST)(DWORD, UINT*, UINT*, RM_PROCESS_INFO[],
+ LPDWORD);
+typedef DWORD(WINAPI* RMENDSESSION)(DWORD);
+} // namespace
+
+namespace mozilla::widget {
+
+class WindowsDebugProcessData : public nsIWindowsDebugProcessData {
+ public:
+ NS_DECL_ISUPPORTS
+
+ WindowsDebugProcessData(const wchar_t* aName, const wchar_t* aFilePath,
+ unsigned long aPid)
+ : mName(aName), mFilePath(aFilePath), mPid(aPid) {}
+
+ NS_IMETHODIMP GetName(nsAString& aOut) {
+ aOut = mName;
+ return NS_OK;
+ }
+
+ NS_IMETHODIMP GetExecutablePath(nsAString& aOut) {
+ aOut = mFilePath;
+ return NS_OK;
+ }
+
+ NS_IMETHODIMP GetPid(uint32_t* aOut) {
+ *aOut = mPid;
+ return NS_OK;
+ }
+
+ private:
+ virtual ~WindowsDebugProcessData() = default;
+ nsString mName;
+ nsString mFilePath;
+ unsigned long mPid;
+};
+
+NS_IMPL_ISUPPORTS(WindowsDebugProcessData, nsIWindowsDebugProcessData)
+NS_IMPL_ISUPPORTS(WindowsTestDebug, nsIWindowsTestDebug)
+
+WindowsTestDebug::WindowsTestDebug() {}
+
+WindowsTestDebug::~WindowsTestDebug() {}
+
+static nsReturnRef<HMODULE> MakeRestartManager() {
+ nsModuleHandle module(::LoadLibraryW(L"Rstrtmgr.dll"));
+ (void)NS_WARN_IF(!module);
+ return module.out();
+}
+
+NS_IMETHODIMP
+WindowsTestDebug::ProcessesThatOpenedFile(
+ const nsAString& aFilename,
+ nsTArray<RefPtr<nsIWindowsDebugProcessData>>& aResult) {
+ nsModuleHandle module(MakeRestartManager());
+ if (!module) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ auto RmStartSession = reinterpret_cast<RMSTARTSESSION>(
+ ::GetProcAddress(module, "RmStartSession"));
+ if (NS_WARN_IF(!RmStartSession)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ auto RmRegisterResources = reinterpret_cast<RMREGISTERRESOURCES>(
+ ::GetProcAddress(module, "RmRegisterResources"));
+ if (NS_WARN_IF(!RmRegisterResources)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ auto RmGetList =
+ reinterpret_cast<RMGETLIST>(::GetProcAddress(module, "RmGetList"));
+ if (NS_WARN_IF(!RmGetList)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ auto RmEndSession =
+ reinterpret_cast<RMENDSESSION>(::GetProcAddress(module, "RmEndSession"));
+ if (NS_WARN_IF(!RmEndSession)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ WCHAR sessionKey[CCH_RM_SESSION_KEY + 1] = {0};
+ DWORD handle;
+ if (NS_WARN_IF(FAILED(RmStartSession(&handle, 0, sessionKey)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto endSession = MakeScopeExit(
+ [&, handle]() { (void)NS_WARN_IF(FAILED(RmEndSession(handle))); });
+
+ LPCWSTR resources[] = {PromiseFlatString(aFilename).get()};
+ if (NS_WARN_IF(FAILED(
+ RmRegisterResources(handle, 1, resources, 0, nullptr, 0, nullptr)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoTArray<RM_PROCESS_INFO, 1> info;
+ UINT numEntries;
+ UINT numEntriesNeeded = 1;
+ DWORD error = ERROR_MORE_DATA;
+ DWORD reason = RmRebootReasonNone;
+ while (error == ERROR_MORE_DATA) {
+ info.SetLength(numEntriesNeeded);
+ numEntries = numEntriesNeeded;
+ error =
+ RmGetList(handle, &numEntriesNeeded, &numEntries, &info[0], &reason);
+ }
+ if (NS_WARN_IF(error != ERROR_SUCCESS)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (UINT i = 0; i < numEntries; ++i) {
+ nsAutoHandle otherProcess(::OpenProcess(
+ PROCESS_QUERY_LIMITED_INFORMATION, FALSE, info[i].Process.dwProcessId));
+ if (NS_WARN_IF(!otherProcess)) {
+ continue;
+ }
+ WCHAR imageName[MAX_PATH];
+ DWORD imageNameLen = MAX_PATH;
+ if (NS_WARN_IF(FAILED(::QueryFullProcessImageNameW(
+ otherProcess, 0, imageName, &imageNameLen)))) {
+ continue;
+ }
+ aResult.AppendElement(new WindowsDebugProcessData(
+ info[i].strAppName, imageName, info[i].Process.dwProcessId));
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::widget
diff --git a/widget/windows/WindowsTestDebug.h b/widget/windows/WindowsTestDebug.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 mozilla_widget_WindowsTestDebug_h__
+#define mozilla_widget_WindowsTestDebug_h__
+
+#include <windows.h>
+#include <restartmanager.h>
+
+#include "nsIWindowsTestDebug.h"
+#include "nsString.h"
+#include "nsWindowsHelpers.h"
+
+namespace mozilla::widget {
+
+class WindowsTestDebug final : public nsIWindowsTestDebug {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIWINDOWSTESTDEBUG
+ WindowsTestDebug();
+
+ private:
+ ~WindowsTestDebug();
+};
+
+} // namespace mozilla::widget
+
+#endif // mozilla_widget_WindowsTestDebug_h__
diff --git a/widget/windows/components.conf b/widget/windows/components.conf
@@ -135,6 +135,14 @@ Classes = [
'headers': ['/widget/windows/SystemStatusBar.h'],
'processes': ProcessSelector.MAIN_PROCESS_ONLY,
},
+ {
+ 'name': 'nsIWindowsTestDebug',
+ 'cid': '{2d63d37b-d0d8-4427-b2ee-3ad706fa923f}',
+ 'contract_ids': ['@mozilla.org/win-test-debug;1'],
+ 'type': 'mozilla::widget::WindowsTestDebug',
+ 'headers': ['/widget/windows/WindowsTestDebug.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
]
if buildconfig.substs['CC_TYPE'] == 'clang-cl':
diff --git a/widget/windows/moz.build b/widget/windows/moz.build
@@ -114,6 +114,7 @@ UNIFIED_SOURCES += [
"WinCompositorWindowThread.cpp",
"WindowHook.cpp",
"WindowsConsole.cpp",
+ "WindowsTestDebug.cpp",
"WinEventObserver.cpp",
"WinIMEHandler.cpp",
"WinMouseScrollHandler.cpp",
@@ -172,6 +173,7 @@ if CONFIG["MOZ_ENABLE_SKIA_PDF"]:
XPIDL_SOURCES += [
"nsIAlertsServiceRust.idl",
+ "nsIWindowsTestDebug.idl",
]
XPIDL_MODULE = "widget_windows"
diff --git a/widget/windows/nsIWindowsTestDebug.idl b/widget/windows/nsIWindowsTestDebug.idl
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(19904b57-e6b8-4c0f-86eb-5ffce4c8d675)]
+interface nsIWindowsDebugProcessData : nsISupports
+{
+ // "User-friendly name" of the process.
+ readonly attribute AString name;
+
+ // Full path to the executable of the process.
+ readonly attribute AString executablePath;
+
+ // PID of the process.
+ readonly attribute unsigned long pid;
+};
+
+// Useful for debugging Windows errors related to resources in use. This class
+// should only be used for debugging purposes.
+[scriptable, uuid(15725b2d-08d1-4b18-9e83-0cccfb87b982)]
+interface nsIWindowsTestDebug : nsISupports
+{
+ /**
+ * Get the list of processes that currently have an open handle to aFilename.
+ * This can be used to determine what other processess (if any) are
+ * preventing access to a file, or even if this process still has open
+ * handles to it. Currently, this list will not include file handles
+ * obtained via memory mapping, which can be kept beyond closing the handle
+ * that they mapped on Windows. This behavior is obviously racy.
+ *
+ * @param aFilepath Full path to the file we are querying.
+ * @returns An array of the processes that held handles to aFilename at the
+ * time the function was executed.
+ */
+ Array<nsIWindowsDebugProcessData> processesThatOpenedFile(in AString aFilename);
+};
diff --git a/widget/windows/tests/unit/test_keep_file_open.worker.js b/widget/windows/tests/unit/test_keep_file_open.worker.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/. */
+
+/* import-globals-from /toolkit/components/workerloader/require.js */
+importScripts("resource://gre/modules/workers/require.js");
+
+const PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
+
+/**
+ *
+ */
+class OpenFileWorker extends PromiseWorker.AbstractWorker {
+ constructor() {
+ super();
+
+ this._file = null;
+ }
+
+ postMessage(message, ...transfers) {
+ self.postMessage(message, transfers);
+ }
+
+ dispatch(method, args) {
+ return this[method](...args);
+ }
+
+ open(path) {
+ this._file = IOUtils.openFileForSyncReading(path);
+ }
+
+ close() {
+ if (this._file) {
+ this._file.close();
+ }
+ }
+}
+
+const worker = new OpenFileWorker();
+
+self.addEventListener("message", msg => worker.handleMessage(msg));
+self.addEventListener("unhandledrejection", err => {
+ throw err.reason;
+});
diff --git a/widget/windows/tests/unit/test_windowsTestDebug.js b/widget/windows/tests/unit/test_windowsTestDebug.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test nsIWindowsTestDebug.
+ */
+
+const { BasePromiseWorker } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseWorker.sys.mjs"
+);
+
+add_task(async () => {
+ const path = await IOUtils.createUniqueFile(PathUtils.tempDir, "openedFile");
+ await IOUtils.writeUTF8(path, "");
+ Assert.ok(await IOUtils.exists(path), path + " should have been created");
+ registerCleanupFunction(async () => {
+ await IOUtils.remove(path);
+ });
+
+ // Use a worker to keep the testFile open.
+ const worker = new BasePromiseWorker(
+ "resource://test/test_keep_file_open.worker.js"
+ );
+ await worker.post("open", [path]);
+
+ // Check that nsIWindowsTestDebug tells us that we have the file open.
+ const WindowsTestDebug = Cc["@mozilla.org/win-test-debug;1"].getService(
+ Ci.nsIWindowsTestDebug
+ );
+ let procs = WindowsTestDebug.processesThatOpenedFile(path);
+ info(JSON.stringify(procs));
+ Assert.equal(procs.length, 1, "Process list contains one process");
+ const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+ );
+ Assert.equal(
+ procs[0].pid,
+ ProcessTools.pid,
+ "Process list contains this process"
+ );
+ await worker.post("close", []);
+ await IOUtils.remove(path);
+});
diff --git a/widget/windows/tests/unit/xpcshell.toml b/widget/windows/tests/unit/xpcshell.toml
@@ -1,4 +1,7 @@
[DEFAULT]
run-if = ["os == 'win'"]
+["test_windowsTestDebug.js"]
+support-files=["test_keep_file_open.worker.js"]
+
["test_windows_alert_service.js"]