Revision control
Copy as Markdown
// 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