Bug 1900726 - Make UpdateMutex cross-platform by using nsProfileLock. r=application-update-reviewers,bytesized,nalexander

This patch implements the new update mutex using nsProfileLock for
all platforms. The logic is implemented in the new nsIUpdateMutex XPCOM
component. Compability with the legacy update mutex is not preserved.

Depends on D222156

Differential Revision: https://phabricator.services.mozilla.com/D222157
This commit is contained in:
Yannis Juglaret 2024-11-15 17:49:50 +00:00
parent ed382c1662
commit b4cea69973
11 changed files with 208 additions and 201 deletions

View File

@ -155,6 +155,13 @@ if defined('MOZ_UPDATER') and not IS_ANDROID:
'processes': ProcessSelector.MAIN_PROCESS_ONLY,
'categories': {'xpcom-startup': 'nsUpdateSyncManager'},
},
{
'cid': '{a553d37f-9a10-496f-a864-d3e4d7d09c3a}',
'contract_ids': ['@mozilla.org/updates/update-mutex;1'],
'type': 'nsUpdateMutex',
'headers': ['/toolkit/xre/nsUpdateMutex.h'],
'processes': ProcessSelector.MAIN_PROCESS_ONLY,
},
]
if not defined('MOZ_DISABLE_PARENTAL_CONTROLS'):

View File

@ -25,7 +25,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
UpdateLog: "resource://gre/modules/UpdateLog.sys.mjs",
UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
ctypes: "resource://gre/modules/ctypes.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
@ -557,9 +556,6 @@ function testWriteAccess(updateTestFile, createDirectory) {
* holding
*/
function hasUpdateMutex() {
if (AppConstants.platform != "win") {
return true;
}
return lazy.UpdateMutex.tryLock();
}
@ -2543,146 +2539,6 @@ class Update {
]);
}
/**
* This class provides an object interface to acquire and release the update
* mutex for the current installation. See nsIUpdateMutex in
* nsIUpdateService.idl for more details.
*/
export class UpdateMutex {
classID = Components.ID("{4545fe83-111d-4b39-8f1e-23e526ff0fd9}");
QueryInterface = ChromeUtils.generateQI([Ci.nsIUpdateMutex]);
#winHandle;
/**
* Constructs an object that can be used to acquire the update mutex. The
* update mutex is not initially acquired after creation. Acquisition
* requires an explicit call to tryLock().
*
* @throws on platforms without a legacy update mutex implementation.
*/
constructor() {
if (AppConstants.platform != "win") {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
this.#winHandle = null;
}
/**
* Checks current acquisition status for the current object.
*
* @returns true if the update mutex for the current installation has
* been acquired by this instance, through the current object.
*/
isLocked() {
return this.#winHandle !== null;
}
/**
* Attempts to acquire the update mutex for the current installation path.
*
* @returns true if the update mutex was successfully acquired.
*/
tryLock() {
if (this.isLocked()) {
return true;
}
this.#winHandle = UpdateMutex.#createMutex(
UpdateMutex.#getPerInstallationMutexName()
);
return this.isLocked();
}
/**
* Releases the legacy update mutex for the current installation path, if
* acquired.
*/
unlock() {
if (this.isLocked()) {
UpdateMutex.#closeHandle(this.#winHandle);
this.#winHandle = null;
}
}
/**
* Windows only function that closes a Win32 handle.
*
* @param handle The handle to close
*/
static #closeHandle(handle) {
if (handle) {
let lib = lazy.ctypes.open("kernel32.dll");
let CloseHandle = lib.declare(
"CloseHandle",
lazy.ctypes.winapi_abi,
lazy.ctypes.int32_t /* success */,
lazy.ctypes.void_t.ptr /* handle */
);
CloseHandle(handle);
lib.close();
}
}
/**
* Windows only function that creates a mutex. Only succeeds if the mutex
* does not already exist.
*
* @param aName
* The name for the mutex.
* @return The Win32 handle to the mutex, or null upon failure.
*/
static #createMutex(aName) {
const INITIAL_OWN = 1;
const ERROR_ALREADY_EXISTS = 0xb7;
let lib = lazy.ctypes.open("kernel32.dll");
let CreateMutexW = lib.declare(
"CreateMutexW",
lazy.ctypes.winapi_abi,
lazy.ctypes.void_t.ptr /* return handle */,
lazy.ctypes.void_t.ptr /* security attributes */,
lazy.ctypes.int32_t /* initial owner */,
lazy.ctypes.char16_t.ptr /* name */
);
let handle = CreateMutexW(null, INITIAL_OWN, aName);
let alreadyExists = lazy.ctypes.winLastError == ERROR_ALREADY_EXISTS;
if (handle && !handle.isNull() && alreadyExists) {
UpdateMutex.#closeHandle(handle);
handle = null;
}
lib.close();
if (handle && handle.isNull()) {
handle = null;
}
return handle;
}
/**
* Windows only function that determines a unique mutex name for the
* installation.
*
* @return Global mutex path
*/
static #getPerInstallationMutexName() {
let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
Ci.nsICryptoHash
);
hasher.init(hasher.SHA1);
let exeFile = Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile);
var data = new TextEncoder().encode(exeFile.path.toLowerCase());
hasher.update(data, data.length);
return "Global\\MozillaUpdateMutex-" + hasher.finish(true);
}
}
export class UpdateService {
#initPromise;
@ -2779,7 +2635,7 @@ export class UpdateService {
case "quit-application":
Services.obs.removeObserver(this, topic);
if (AppConstants.platform == "win" && lazy.UpdateMutex.isLocked()) {
if (lazy.UpdateMutex.isLocked()) {
// If we hold the update mutex, let it go!
// The OS would clean this up sometime after shutdown,
// but that would have no guarantee on timing.
@ -2810,7 +2666,7 @@ export class UpdateService {
break;
case "test-unlock-update-mutex":
if (Cu.isInAutomation) {
if (AppConstants.platform == "win" && lazy.UpdateMutex.isLocked()) {
if (lazy.UpdateMutex.isLocked()) {
LOG("UpdateService:observe - releasing update mutex for testing");
lazy.UpdateMutex.unlock();
}

View File

@ -27,13 +27,6 @@ Classes = [
'singleton': True,
},
{
'cid': '{4545fe83-111d-4b39-8f1e-23e526ff0fd9}',
'contract_ids': ['@mozilla.org/updates/update-mutex;1'],
'esModule': 'resource://gre/modules/UpdateService.sys.mjs',
'constructor': 'UpdateMutex',
},
{
'cid': '{e43b0010-04ba-4da6-b523-1f92580bc150}',
'contract_ids': ['@mozilla.org/updates/update-service-stub;1'],

View File

@ -62,7 +62,6 @@ tags = "os_integration"
["browser_aboutDialog_fc_check_noUpdate.js"]
["browser_aboutDialog_fc_check_otherInstance.js"]
run-if = ["os == 'win' && !msix"] # "Windows only feature."
tags = "os_integration"
["browser_aboutDialog_fc_check_unsupported.js"]
@ -118,7 +117,6 @@ tags = "os_integration"
["browser_aboutPrefs_fc_check_noUpdate.js"]
["browser_aboutPrefs_fc_check_otherInstance.js"]
run-if = ["os == 'win' && !msix"] # "test must be able to prevent file deletion."
tags = "os_integration"
["browser_aboutPrefs_fc_check_unsupported.js"]

View File

@ -201,14 +201,9 @@ function lockWriteTestFile() {
* acquires it through a fresh nsIUpdateMutex object, so the update code thinks
* there is another instance handling updates.
*
* @throws If the function is called on a platform other than Windows, or if we
* fail to acquire the update mutex.
* @throws If acquiring the update mutex fails.
*/
function setOtherInstanceHandlingUpdates() {
if (AppConstants.platform != "win") {
throw new Error("Windows only test function called");
}
gAUS.observe(null, "test-unlock-update-mutex", "");
let updateMutex = Cc["@mozilla.org/updates/update-mutex;1"].createInstance(

View File

@ -15,37 +15,35 @@ async function run_test() {
testFile.remove(false);
Assert.ok(!testFile.exists(), MSG_SHOULD_NOT_EXIST);
let updateMutex = Cc["@mozilla.org/updates/update-mutex;1"].createInstance(
Ci.nsIUpdateMutex
);
// Acquire the update mutex to prevent the current instance from being
// able to check for or apply updates -- and check that it can't.
if (AppConstants.platform == "win") {
let updateMutex = Cc["@mozilla.org/updates/update-mutex;1"].createInstance(
Ci.nsIUpdateMutex
);
debugDump("attempting to acquire the update mutex");
Assert.ok(
updateMutex.tryLock(),
"should be able to acquire the update mutex"
);
debugDump("attempting to acquire the update mutex");
try {
// Check that available updates cannot be checked for when the update
// mutex for this installation path is acquired.
Assert.ok(
updateMutex.tryLock(),
"should be able to acquire the update mutex"
!gAUS.canCheckForUpdates,
"should not be able to check for updates when the update mutex is acquired by another instance"
);
try {
// Check that available updates cannot be checked for when the update
// mutex for this installation path is acquired.
Assert.ok(
!gAUS.canCheckForUpdates,
"should not be able to check for updates when the update mutex is acquired by another instance"
);
// Check if updates cannot be applied when the update mutex for this
// installation path is acquired.
Assert.ok(
!gAUS.canApplyUpdates,
"should not be able to apply updates when the update mutex is acquired by another instance"
);
} finally {
debugDump("releasing the update mutex");
updateMutex.unlock();
}
// Check if updates cannot be applied when the update mutex for this
// installation path is acquired.
Assert.ok(
!gAUS.canApplyUpdates,
"should not be able to apply updates when the update mutex is acquired by another instance"
);
} finally {
debugDump("releasing the update mutex");
updateMutex.unlock();
}
// Check that available updates can be checked for
@ -55,22 +53,16 @@ async function run_test() {
// Attempt to acquire the update mutex(es) now that the current instance has
// acquired it.
if (AppConstants.platform == "win") {
let updateMutex = Cc["@mozilla.org/updates/update-mutex;1"].createInstance(
Ci.nsIUpdateMutex
);
debugDump("attempting to acquire the update mutex");
let isAcquired = updateMutex.tryLock();
if (isAcquired) {
updateMutex.unlock();
}
Assert.ok(
!isAcquired,
"should not be able to acquire the update mutex when the current instance has already acquired it"
);
debugDump("attempting to acquire the update mutex");
let isAcquired = updateMutex.tryLock();
if (isAcquired) {
updateMutex.unlock();
}
Assert.ok(
!isAcquired,
"should not be able to acquire the update mutex when the current instance has already acquired it"
);
await doTestFinish();
}

View File

@ -20,6 +20,8 @@ XPIDL_SOURCES += [
XPIDL_MODULE = "toolkitprofile"
EXPORTS += ["nsProfileLock.h"]
UNIFIED_SOURCES += ["nsProfileLock.cpp"]
if CONFIG["OS_ARCH"] == "WINNT":

View File

@ -156,6 +156,7 @@ if CONFIG["MOZ_UPDATER"]:
if CONFIG["MOZ_WIDGET_TOOLKIT"] != "android":
UNIFIED_SOURCES += [
"nsUpdateDriver.cpp",
"nsUpdateMutex.cpp",
"nsUpdateSyncManager.cpp",
]

View File

@ -0,0 +1,83 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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 "nsUpdateMutex.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/StaticMutex.h"
#include "nsIFile.h"
#include "nsProfileLock.h"
#include "nsXULAppAPI.h"
mozilla::StaticMutex UpdateMutexImpl::sInProcessMutex;
bool UpdateMutexImpl::TryLock() {
if (!sInProcessMutex.TryLock()) {
return false;
}
bool success = [&crossProcessLock = mCrossProcessLock]() {
nsCOMPtr<nsIFile> updRoot;
nsresult nsrv =
NS_GetSpecialDirectory(XRE_UPDATE_ROOT_DIR, getter_AddRefs(updRoot));
if (NS_FAILED(nsrv)) {
return false;
}
nsrv = updRoot->Create(nsIFile::DIRECTORY_TYPE, 0755);
if (NS_FAILED(nsrv) && nsrv != NS_ERROR_FILE_ALREADY_EXISTS) {
return false;
}
return NS_SUCCEEDED(crossProcessLock.Lock(updRoot, nullptr));
}();
if (!success) {
sInProcessMutex.Unlock();
}
return success;
}
void UpdateMutexImpl::Unlock() {
sInProcessMutex.AssertCurrentThreadOwns();
mozilla::DebugOnly<nsresult> nsrv = mCrossProcessLock.Unlock();
#ifdef DEBUG
if (!NS_SUCCEEDED(nsrv)) {
MOZ_CRASH_UNSAFE_PRINTF(
"failed to unlock the update mutex's nsProfileLock -- got 0x%08x",
static_cast<uint32_t>(nsrv.inspect()));
}
#endif
sInProcessMutex.Unlock();
}
NS_IMPL_ISUPPORTS(nsUpdateMutex, nsIUpdateMutex)
NS_IMETHODIMP nsUpdateMutex::IsLocked(bool* aResult) {
*aResult = mIsLocked;
return NS_OK;
}
NS_IMETHODIMP nsUpdateMutex::TryLock(bool* aResult) {
if (!mIsLocked) {
mIsLocked = mUpdateMutexImpl.TryLock();
}
*aResult = mIsLocked;
return NS_OK;
}
NS_IMETHODIMP nsUpdateMutex::Unlock() MOZ_NO_THREAD_SAFETY_ANALYSIS {
// Thread safety analysis cannot make sense out of a conditional release
if (mIsLocked) {
mUpdateMutexImpl.Unlock();
mIsLocked = false;
}
return NS_OK;
}

View File

@ -0,0 +1,76 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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 nsUpdateMutex_h__
#define nsUpdateMutex_h__
#include "nsIUpdateService.h"
#include "nsProfileLock.h"
#include "mozilla/StaticMutex.h"
/**
* A primitive object type suitable for acquiring the update mutex. It is
* composed of two parts:
* - a nsProfileLock taken on the update directory, to ensure that if two
* instances running from the same application path try to acquire the
* update mutex simultaneously, only one of them succeeds;
* - a StaticMutex, to ensure that even within the same instance of the
* application, it is never possible to successfully acquire two
* UpdateMutexImpl objects simultaneously.
*
* While the second part is not strictly required, it makes reasoning about
* these objects easier, and it helps us simulate an acquisition coming from
* another instance in tests.
*
* Contrary to a nsIUpdateMutex object, an UpdateMutexImpl object does not
* keep track of whether it is currently locked or unlocked. Therefore, it is
* the responsibility of the caller to guarantee the following:
* - a call to Unlock() must only occur after a matching successful call to
* TryLock();
* - no second call to TryLock() should ever occur after a successful first
* call to TryLock(), unless a call to Unlock() occured in the middle.
*/
class MOZ_CAPABILITY("mutex") UpdateMutexImpl {
public:
[[nodiscard]] bool TryLock() MOZ_TRY_ACQUIRE(true);
void Unlock() MOZ_CAPABILITY_RELEASE();
private:
static mozilla::StaticMutex sInProcessMutex;
nsProfileLock mCrossProcessLock;
};
/**
* An XPCOM wrapper for the UpdateMutexImpl primitive type, achieving the same
* goals but through a safe XPCOM-compatible nsIUpdateMutex interface.
*
* Contrary to UpdateMutexImpl objects, nsUpdateMutex objects track whether
* they are currently locked or unlocked. It is therefore always safe to call
* TryLock() or Unlock() on a nsUpdateMutex object.
*
* See nsIUpdateMutex in nsUpdateService.idl for more details.
*/
class nsUpdateMutex final : public nsIUpdateMutex {
public:
explicit nsUpdateMutex() = default;
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIUPDATEMUTEX
private:
UpdateMutexImpl mUpdateMutexImpl;
bool mIsLocked{};
virtual ~nsUpdateMutex() {
if (mIsLocked) {
Unlock();
}
}
};
#endif // nsUpdateMutex_h__

View File

@ -38,6 +38,10 @@ class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS MOZ_CAPABILITY("mutex")
void Lock() MOZ_CAPABILITY_ACQUIRE() { Mutex()->Lock(); }
[[nodiscard]] bool TryLock() MOZ_TRY_ACQUIRE(true) {
return Mutex()->TryLock();
}
void Unlock() MOZ_CAPABILITY_RELEASE() { Mutex()->Unlock(); }
void AssertCurrentThreadOwns() MOZ_ASSERT_CAPABILITY(this) {