Revision control
++ // Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
++ // Copyright (C) 2017 Intel Corporation.
++ // Copyright (C) 2016 The Qt Company Ltd.
++ // 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 "QtCore/qdebug.h"
++ #include "QtCore/qdatetime.h"
++ #include "QtCore/qfileinfo.h"
++ #if defined(Q_OS_HARMONY)
++ #include "QtCore/qcoreapplication.h"
++ #endif
++
++ #include "private/qcore_unix_p.h" // qt_safe_open
++ #include "private/qfilesystementry_p.h"
++ #include "private/quniquehandle_types_p.h"
++
++ #if !defined(Q_OS_INTEGRITY)
++ #include <sys/file.h> // flock
++ #endif
++
++ #if defined(Q_OS_RTEMS)
++ // flock() does not work in these OSes and produce warnings when we try to use
++ # undef LOCK_EX
++ # undef LOCK_NB
++ #endif
++
++ #include <sys/types.h> // kill
++ #include <signal.h> // kill
++ #include <unistd.h> // gethostname
++
++ #if defined(Q_OS_MACOS)
++ # include <libproc.h>
++ #elif defined(Q_OS_LINUX)
++ # include <unistd.h>
++ # include <cstdio>
++ #elif defined(Q_OS_HAIKU)
++ # include <kernel/OS.h>
++ #elif defined(Q_OS_BSD4) && !defined(QT_PLATFORM_UIKIT)
++ # include <sys/cdefs.h>
++ # include <sys/param.h>
++ # include <sys/sysctl.h>
++ # if !defined(Q_OS_NETBSD)
++ # include <sys/user.h>
++ # endif
++ #endif
++
++ #ifndef O_DIRECTORY
++ # define O_DIRECTORY 0
++ #endif
++ #ifndef O_PATH
++ # define O_PATH 0
++ #endif
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ // ### merge into qt_safe_write?
++ static qint64 qt_write_loop(int fd, const char *data, qint64 len)
++ {
++ qint64 pos = 0;
++ while (pos < len) {
++ const qint64 ret = qt_safe_write(fd, data + pos, len - pos);
++ if (ret == -1) // e.g. partition full
++ return pos;
++ pos += ret;
++ }
++ return pos;
++ }
++
++ /*
++ * Details about file locking on Unix.
++ *
++ * There are three types of advisory locks on Unix systems:
++ * 1) POSIX process-wide locks using fcntl(F_SETLK)
++ * 2) BSD flock(2) system call
++ * 3) Linux-specific file descriptor locks using fcntl(F_OFD_SETLK)
++ * There's also a mandatory locking feature by POSIX, which is deprecated on
++ * Linux and users are advised not to use it.
++ *
++ * The first problem is that the POSIX API is braindead. POSIX.1-2008 says:
++ *
++ * All locks associated with a file for a given process shall be removed when
++ * a file descriptor for that file is closed by that process or the process
++ * holding that file descriptor terminates.
++ *
++ * The Linux manpage is clearer:
++ *
++ * * If a process closes _any_ file descriptor referring to a file, then all
++ * of the process's locks on that file are released, regardless of the file
++ * descriptor(s) on which the locks were obtained. This is bad: [...]
++ *
++ * * The threads in a process share locks. In other words, a multithreaded
++ * program can't use record locking to ensure that threads don't
++ * simultaneously access the same region of a file.
++ *
++ * So in order to use POSIX locks, we'd need a global mutex that stays locked
++ * while the QLockFile is locked. For that reason, Qt does not use POSIX
++ * advisory locks anymore.
++ *
++ * The next problem is that POSIX leaves undefined the relationship between
++ * locks with fcntl(), flock() and lockf(). In some systems (like the BSDs),
++ * all three use the same record set, while on others (like Linux) the locks
++ * are independent, except if locking over NFS mounts, in which case they're
++ * actually the same. Therefore, it's a very bad idea to mix them in the same
++ * process.
++ *
++ * We therefore use only flock(2), except on Android.
++ *
++ * Android Compatibility:
++ * Some versions of Android have known issues where flock does not function correctly.
++ * As a result, on Android, we use POSIX fcntl(F_SETLK) to handle file locking.
++ * fcntl is better integrated with Android’s underlying system, avoiding
++ * the limitations of flock.
++ *
++ * Race conditions and mitigations:
++ *
++ * 1) Concurrent creation of the lock file
++ * Creation of the lock file requires O_EXCL flag, which causes the OS to
++ * arbitrate. Only the thread that succeeded creating the lock file with this
++ * flag can claim to have locked.
++ *
++ * 2) Concurrent setNativeLocks()
++ * (Assuming an implementation that works, see main class docs for known
++ * limitations)
++ * This is applicable to tryLock_sys() and removeStaleLock() concurrency, as
++ * tryLock_sys() will only attempt to lock if open(O_EXCL) succeeded. The OS
++ * arbitrates and ensures only one thread at a time can lock the file's
++ * contents. QLockFile success implies the native lock either succeeded too or
++ * wasn't supported (definitely did not fail to lock).
++ *
++ * 3) Lock creation and unlink() race
++ * This is also applicable to tryLock_sys() and removeStaleLock() concurrency:
++ * following from the last case, removeStaleLock() may have natively-locked the
++ * file, unlinked it, then unlocked, all before tryLock_sys() reached
++ * setNativeLocks(). At this point, the lock file no longer exists in the
++ * directory but setNativeLocks() may yet succeed. This race is unavoidable, so
++ * we re-verify at the end whether the lock file is ours: if it is, then it has
++ * the lock and shouldn't get stolen.
++ */
++
++ namespace {
++ enum class NativeLocking {
++ UnknownError = -2,
++ NotSupported = -1,
++ Failed = 0,
++ Locked = 1,
++ };
++ }
++
++ static NativeLocking setNativeLocks(int fd)
++ {
++ int ret;
++ #if defined(Q_OS_ANDROID)
++ struct flock fl;
++ fl.l_type = F_WRLCK;
++ fl.l_whence = SEEK_SET;
++ fl.l_start = 0;
++ fl.l_len = 0;
++ QT_EINTR_LOOP(ret, fcntl(fd, F_SETLK, &fl));
++ #elif defined(LOCK_EX) && defined(LOCK_NB)
++ // applies to other threads, and other processes on a local fs
++ QT_EINTR_LOOP(ret, flock(fd, LOCK_EX | LOCK_NB));
++ #else
++ Q_UNUSED(fd);
++ ret = -1;
++ errno = ENOTSUP;
++ #endif
++
++ if (ret == 0)
++ return NativeLocking::Locked;
++
++ // failed to lock, but why?
++ switch (errno) {
++ case EWOULDBLOCK: // flock() documented to use this
++ #if EAGAIN != EWOULDBLOCK
++ case EAGAIN: // F_SETLK documented to use this
++ #endif
++ case EACCES: // F_SETLK documented to possibly use this too
++ return NativeLocking::Failed;
++
++ case ENOLCK:
++ // This can be both a resource error (lock table is full) and locking
++ // is not possible (e.g., rpc.lockd unable to lock). We err on the side
++ // of the permanence (more likely).
++ return NativeLocking::NotSupported;
++
++ case EINVAL:
++ case ENOTSUP:
++ case ENOSYS:
++ return NativeLocking::NotSupported;
++ }
++
++ return NativeLocking::UnknownError;
++ }
++
++ static QByteArrayView baseName(const QByteArray &fileName)
++ {
++ // assert that the incoming string is null-terminated
++ Q_ASSERT(fileName.data()[fileName.size()] == '\0');
++
++ // find the last separator
++ qsizetype sep = fileName.lastIndexOf('/');
++ if (sep < 0)
++ return fileName; // relative path is *just* the file name
++ return QByteArrayView(fileName).sliced(sep + 1);
++ }
++
++ static bool releaseLockFile(int fileHandle, int dirfd, const QByteArray &fullFileName)
++ {
++ #ifdef AT_FDCWD
++ if (dirfd < 0)
++ dirfd = AT_FDCWD;
++ #else
++ // fake it
++ auto unlinkat = [](int fd, const char *name, int /*flags*/) {
++ Q_ASSERT(fd < 0);
++ return unlink(name);
++ };
++ #endif
++
++ QByteArrayView fileName = fullFileName;
++ if (dirfd >= 0)
++ fileName = baseName(fullFileName);
++
++ // first we try to unlink the file while it's locked
++ int ret = unlinkat(dirfd, fileName.data(), 0);
++ int savedErrno = ret == 0 ? 0 : errno;
++ qt_safe_close(fileHandle);
++
++ switch (savedErrno) {
++ // we have observed on some systems we can't unlink a file with an
++ // active lock, so we must close it first, then unlink
++ case EBUSY: // observed with some NFS servers
++ case ETXTBSY: // not observed, just defensive
++ case EPERM: // probably can't unlink() an open file (independent of locks)
++ ret = unlinkat(dirfd, fileName.data(), 0);
++ savedErrno = ret == 0 ? 0 : errno;
++ }
++
++ if (savedErrno == 0 || savedErrno == ENOENT || savedErrno == ENOTDIR)
++ return true; // file is gone
++
++ qWarning("Could not remove our own lock file %s: %ls (maybe permissions changed meanwhile?)",
++ fileName.cbegin(), qUtf16Printable(qt_error_string(savedErrno)));
++ // This is bad because other users of this lock file will now have to wait for the stale-lock-timeout...
++
++ return false;
++ }
++
++ // This function is designed to deal with race conditions coming from two (or
++ // more) threads/processes accessing the lock file at the same time, either in
++ // tryLock_sys() itself or removeStaleLock() - both called by tryLock().
++ QLockFile::LockError QLockFilePrivate::tryLock_sys(const QLockFilePrivate::LockFileInfo ¤t)
++ {
++ constexpr int OpenFlags = QT_OPEN_RDWR | QT_OPEN_CREAT | QT_OPEN_EXCL;
++ const QByteArray lockFileName = QFile::encodeName(fileName);
++ QUniqueFileDescriptorHandle dirfd, fd;
++
++ #ifdef AT_FDCWD
++ QByteArrayView base = baseName(lockFileName);
++ {
++ // attempt to open the directory where the file name will be
++ constexpr int DirOpenFlags = QT_OPEN_RDONLY | O_DIRECTORY | O_PATH | O_CLOEXEC;
++ QByteArray dirName;
++ if (qsizetype dirNameLen = lockFileName.size() - base.size())
++ dirName = lockFileName.first(dirNameLen); // including slash
++ else
++ dirName = "."_ba;
++
++ dirfd.reset(qt_safe_open(dirName.constBegin(), DirOpenFlags));
++ if (dirfd.get() >= 0) {
++ int ret;
++ QT_EINTR_LOOP(ret, openat(dirfd.get(), base.begin(), OpenFlags | O_CLOEXEC, 0666));
++ fd.reset(ret); // may be -1
++ }
++ }
++ #endif
++
++ if (dirfd.get() < 0)
++ fd.reset(qt_safe_open(lockFileName.constBegin(), OpenFlags, 0666));
++ if (fd.get() < 0) {
++ switch (errno) {
++ case EEXIST:
++ return QLockFile::LockFailedError;
++ case EACCES:
++ case EROFS:
++ return QLockFile::PermissionError;
++ default:
++ return QLockFile::UnknownError;
++ }
++ }
++ // Ensure nobody else can delete the file while we have it
++ if (NativeLocking r = setNativeLocks(fd.get()); r == NativeLocking::Failed) {
++ // someone holds this lock
++ return QLockFile::LockFailedError;
++ } else if (r == NativeLocking::UnknownError) {
++ const int errnoSaved = errno;
++ qWarning() << "setNativeLocks failed:" << qt_error_string(errnoSaved);
++ }
++
++ if (QByteArray fileData = current.asFileContents();
++ qt_write_loop(fd.get(), fileData.constData(), fileData.size()) < fileData.size()) {
++ releaseLockFile(fd.release(), dirfd.get(), lockFileName);
++ return QLockFile::UnknownError; // partition full
++ }
++
++ // Confirm the lock file is our file.
++ QT_STATBUF fromFs = {}, fromFd = {};
++ #if defined(AT_FDCWD) && defined(QT_FSTATAT)
++ if (dirfd.get() >= 0 && QT_FSTATAT(dirfd.get(), base.begin(), &fromFs, 0) < 0) {
++ // probably ENOENT, meaning the file has disappeared
++ return QLockFile::LockFailedError;
++ }
++ #endif
++ if (fromFs.st_dev == 0 && QT_STAT(lockFileName.constData(), &fromFs) < 0) {
++ // probably ENOENT, meaning the file has disappeared
++ return QLockFile::LockFailedError;
++ }
++ std::ignore = QT_FSTAT(fd.get(), &fromFd);
++ if (fromFd.st_dev != fromFs.st_dev || fromFd.st_ino != fromFs.st_ino) {
++ // lock file was replaced, stolen from under us
++ return QLockFile::LockFailedError;
++ }
++
++ // We hold the lock, continue.
++ fileHandle = fd.release();
++ dirHandle = dirfd.release();
++
++ // Sync to disk if possible. Ignore errors (e.g. not supported).
++ #if defined(_POSIX_SYNCHRONIZED_IO) && _POSIX_SYNCHRONIZED_IO > 0
++ fdatasync(fileHandle);
++ #else
++ fsync(fileHandle);
++ #endif
++
++ return QLockFile::NoError;
++ }
++
++ bool QLockFilePrivate::removeStaleLock()
++ {
++ const QByteArray lockFileName = QFile::encodeName(fileName);
++ const int fd = qt_safe_open(lockFileName.constData(), O_WRONLY, 0666);
++ if (fd < 0) // gone already?
++ return errno == ENOENT;
++
++ if (setNativeLocks(fd) != NativeLocking::Failed) {
++ // if we applied native locks, the file was stale; do delete it
++ return releaseLockFile(fd, -1, lockFileName);
++ }
++
++ // there was a lock, so it's not stale
++ qt_safe_close(fd);
++ return false;
++ }
++
++ bool QLockFilePrivate::isProcessRunning(qint64 pid, const QString &appname)
++ {
++ if (::kill(pid_t(pid), 0) == -1 && errno == ESRCH)
++ return false; // PID doesn't exist anymore
++
++ const QString processName = processNameByPid(pid);
++ if (!processName.isEmpty()) {
++ QFileInfo fi(appname);
++ if (fi.isSymLink())
++ fi.setFile(fi.symLinkTarget());
++ if (processName != fi.fileName())
++ return false; // PID got reused by a different application.
++ }
++
++ return true;
++ }
++
++ QString QLockFilePrivate::processNameByPid(qint64 pid)
++ {
++ #if defined(Q_OS_MACOS)
++ char name[1024];
++ proc_name(pid, name, sizeof(name) / sizeof(char));
++ return QFile::decodeName(name);
++ #elif defined(Q_OS_HARMONY)
++ // On OHOS, all Qt applications are started by the appspawn daemon via fork() + dlopen(),
++ // so /proc/{pid}/exe always resolves to "appspawn" rather than the actual application.
++ // For the current process, qAppName() is used instead, which is set by the OHOS QPA plugin
++ // to the loaded .so path (e.g. libMyApp.so). For external processes, /proc/{pid}/... is
++ // inaccessible due to sandboxing, so an empty string is returned; this causes
++ // isProcessRunning() to skip the name comparison and rely solely on PID existence.
++ // Note: this means PID reuse by an external process is not detected.
++ return pid == ::getpid() ? qAppName() : QString();
++ #elif defined(Q_OS_LINUX)
++ if (!qt_haveLinuxProcfs())
++ return QString();
++
++ char exePath[64];
++ sprintf(exePath, "/proc/%lld/exe", pid);
++
++ QByteArray buf = qt_readlink(exePath);
++ if (buf.isEmpty()) {
++ // The pid is gone. Return some invalid process name to fail the test.
++ return QStringLiteral("/ERROR/");
++ }
++
++ // remove the " (deleted)" suffix, if any
++ static const char deleted[] = " (deleted)";
++ if (buf.endsWith(deleted))
++ buf.chop(strlen(deleted));
++
++ return QFileSystemEntry(buf, QFileSystemEntry::FromNativePath()).fileName();
++ #elif defined(Q_OS_HAIKU)
++ thread_info info;
++ if (get_thread_info(pid, &info) != B_OK)
++ return QString();
++ return QFile::decodeName(info.name);
++ #elif defined(Q_OS_BSD4) && !defined(QT_PLATFORM_UIKIT)
++ # if defined(Q_OS_NETBSD)
++ struct kinfo_proc2 kp;
++ int mib[6] = { CTL_KERN, KERN_PROC2, KERN_PROC_PID, (int)pid, sizeof(struct kinfo_proc2), 1 };
++ # elif defined(Q_OS_OPENBSD)
++ struct kinfo_proc kp;
++ int mib[6] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, (int)pid, sizeof(struct kinfo_proc), 1 };
++ # else
++ struct kinfo_proc kp;
++ int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, (int)pid };
++ # endif
++ size_t len = sizeof(kp);
++ u_int mib_len = sizeof(mib)/sizeof(u_int);
++
++ if (sysctl(mib, mib_len, &kp, &len, NULL, 0) < 0)
++ return QString();
++
++ # if defined(Q_OS_OPENBSD) || defined(Q_OS_NETBSD)
++ if (kp.p_pid != pid)
++ return QString();
++ QString name = QFile::decodeName(kp.p_comm);
++ # else
++ if (kp.ki_pid != pid)
++ return QString();
++ QString name = QFile::decodeName(kp.ki_comm);
++ # endif
++ return name;
++ #elif defined(Q_OS_QNX)
++ char exePath[PATH_MAX];
++ sprintf(exePath, "/proc/%lld/exefile", pid);
++
++ int fd = qt_safe_open(exePath, O_RDONLY);
++ if (fd == -1)
++ return QString();
++
++ QT_STATBUF sbuf;
++ if (QT_FSTAT(fd, &sbuf) == -1) {
++ qt_safe_close(fd);
++ return QString();
++ }
++
++ QByteArray buffer(sbuf.st_size, Qt::Uninitialized);
++ buffer.resize(qt_safe_read(fd, buffer.data(), sbuf.st_size - 1));
++ if (buffer.isEmpty()) {
++ // The pid is gone. Return some invalid process name to fail the test.
++ return QStringLiteral("/ERROR/");
++ }
++ return QFileSystemEntry(buffer, QFileSystemEntry::FromNativePath()).fileName();
++ #else
++ Q_UNUSED(pid);
++ return QString();
++ #endif
++ }
++
++ int QLockFilePrivate::openNewFileDescriptor(const QString &fileName)
++ {
++ return QT_OPEN(fileName.toLocal8Bit().constData(), QT_OPEN_RDONLY);
++ }
++
++ void QLockFile::unlock()
++ {
++ Q_D(QLockFile);
++ if (!d->isLocked)
++ return;
++
++ const QByteArray lockFileName = QFile::encodeName(d->fileName);
++ releaseLockFile(d->fileHandle, d->dirHandle, lockFileName);
++ if (d->dirHandle != -1)
++ close(d->dirHandle);
++ d->fileHandle = -1;
++ d->dirHandle = -1;
++ d->lockError = QLockFile::NoError;
++ d->isLocked = false;
++ }
++
++ QT_END_NAMESPACE