// Copyright (C) 2013 David Faure // Copyright (C) 2016 The Qt Company Ltd. // Copyright (C) 2017 Intel Corporation. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only // Qt-Security score:significant reason:default #include "private/qlockfile_p.h" #include "private/qfilesystementry_p.h" #include "wcharhelpers_win_p.h" #include "QtCore/qdatetime.h" #include "QtCore/qdir.h" #include "QtCore/qdebug.h" #include "QtCore/qfileinfo.h" #include "QtCore/qthread.h" #include #include #include #include QT_BEGIN_NAMESPACE static inline bool fileExists(const wchar_t *fileName) { WIN32_FILE_ATTRIBUTE_DATA data; return GetFileAttributesEx(fileName, GetFileExInfoStandard, &data); } static bool deleteFile(const QString &fileName) { const DWORD dwShareMode = 0; // no sharing SECURITY_ATTRIBUTES securityAtts = { sizeof(SECURITY_ATTRIBUTES), NULL, FALSE }; HANDLE fh = CreateFile(qt_castToWchar(QDir::toNativeSeparators(fileName)), GENERIC_READ | GENERIC_WRITE, dwShareMode, &securityAtts, OPEN_EXISTING, // error if it doesn't exist FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE, NULL); bool success = (fh != INVALID_HANDLE_VALUE); if (success) { CloseHandle(fh); // the file is now deleted } else { const DWORD lastError = GetLastError(); if (lastError == ERROR_FILE_NOT_FOUND) success = true; } return success; } QLockFile::LockError QLockFilePrivate::tryLock_sys() { const QFileSystemEntry fileEntry(fileName); // When writing, allow others to read. // When reading, QFile will allow others to read and write, all good. // ### Open the file with DELETE permission and use // SetFileInformationByHandle to delete the file without needing to close // the handle first, to avoid someone opening the handle again without the // FILE_SHARE_DELETE flag in-between closure and deletion. const DWORD dwShareMode = FILE_SHARE_READ; SECURITY_ATTRIBUTES securityAtts = { sizeof(SECURITY_ATTRIBUTES), NULL, FALSE }; HANDLE fh = CreateFile((const wchar_t*)fileEntry.nativeFilePath().utf16(), GENERIC_READ | GENERIC_WRITE, dwShareMode, &securityAtts, CREATE_NEW, // error if already exists FILE_ATTRIBUTE_NORMAL, NULL); if (fh == INVALID_HANDLE_VALUE) { const DWORD lastError = GetLastError(); switch (lastError) { case ERROR_SHARING_VIOLATION: case ERROR_ALREADY_EXISTS: case ERROR_FILE_EXISTS: return QLockFile::LockFailedError; case ERROR_ACCESS_DENIED: // readonly file, or file still in use by another process. // Assume the latter if the file exists, since we don't create it readonly. return fileExists((const wchar_t*)fileEntry.nativeFilePath().utf16()) ? QLockFile::LockFailedError : QLockFile::PermissionError; default: qWarning("Got unexpected locking error %llu", quint64(lastError)); return QLockFile::UnknownError; } } // We hold the lock, continue. fileHandle = fh; QByteArray fileData = lockFileContents(); DWORD bytesWritten = 0; QLockFile::LockError error = QLockFile::NoError; if (!WriteFile(fh, fileData.constData(), fileData.size(), &bytesWritten, NULL) || !FlushFileBuffers(fh)) error = QLockFile::UnknownError; // partition full return error; } bool QLockFilePrivate::removeStaleLock() { // DeleteFile fails if the other process is still using the file, so it's not stale. return deleteFile(fileName); } bool QLockFilePrivate::isProcessRunning(qint64 pid, const QString &appname) { HANDLE procHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); if (!procHandle) return false; // We got a handle but check if process is still alive DWORD exitCode = 0; if (!::GetExitCodeProcess(procHandle, &exitCode)) exitCode = 0; ::CloseHandle(procHandle); if (exitCode != STILL_ACTIVE) return false; const QString processName = processNameByPid(pid); if (!processName.isEmpty() && processName != appname) return false; // PID got reused by a different application. return true; } QString QLockFilePrivate::processNameByPid(qint64 pid) { HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, DWORD(pid)); if (!hProcess) { return QString(); } wchar_t buf[MAX_PATH]; const DWORD length = GetModuleFileNameExW(hProcess, NULL, buf, sizeof(buf) / sizeof(wchar_t)); CloseHandle(hProcess); if (!length) return QString(); QString name = QString::fromWCharArray(buf, length); int i = name.lastIndexOf(u'\\'); if (i >= 0) name.remove(0, i + 1); i = name.lastIndexOf(u'.'); if (i >= 0) name.truncate(i); return name; } int QLockFilePrivate::openNewFileDescriptor(const QString &fileName) { // We currently open with FILE_SHARE_DELETE, which would allow deletion to // be requested even while other processes have the file open. We mostly // want to do this so we can later open the file with the DELETE permission // to delete the file using SetFileInformationByHandle, avoiding the need // to close the handle first, where e.g. search indexer or antivirus may // see their chance to open the file before we can delete it. // We can't make this change immediately because currently-deployed // applications will not be using FILE_SHARE_DELETE, so they would suddenly // be unable to read the lockfile information. HANDLE handle = CreateFile(reinterpret_cast(fileName.utf16()), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) return -1; int fd = _open_osfhandle(intptr_t(handle), _O_RDONLY); if (fd == -1) { CloseHandle(handle); return -1; } return fd; } void QLockFile::unlock() { Q_D(QLockFile); if (!d->isLocked) return; CloseHandle(d->fileHandle); int attempts = 0; static const int maxAttempts = 500; // 500ms while (!deleteFile(d->fileName) && ++attempts < maxAttempts) { // Someone is reading the lock file right now (on Windows this prevents deleting it). QThread::msleep(1); } if (attempts == maxAttempts) { qWarning() << "Could not remove our own lock file" << d->fileName << ". Either other users of the lock file are reading it constantly for 500 ms, " "or we (no longer) have permissions to delete the file"; // This is bad because other users of this lock file will now have to wait for the // stale-lock-timeout... } d->lockError = QLockFile::NoError; d->isLocked = false; } QT_END_NAMESPACE