Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qlockfile_unix.cpp
Go to the documentation of this file.
1// Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
2// Copyright (C) 2017 Intel Corporation.
3// Copyright (C) 2016 The Qt Company Ltd.
4// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
5// Qt-Security score:significant reason:default
6
7#include "private/qlockfile_p.h"
8
9#include "QtCore/qdebug.h"
10#include "QtCore/qdatetime.h"
11#include "QtCore/qfileinfo.h"
12#if defined(Q_OS_HARMONY)
13#include "QtCore/qcoreapplication.h"
14#endif
15
16#include "private/qcore_unix_p.h" // qt_safe_open
17#include "private/qfilesystementry_p.h"
18#include "private/quniquehandle_types_p.h"
19
20#if !defined(Q_OS_INTEGRITY)
21#include <sys/file.h> // flock
22#endif
23
24#if defined(Q_OS_RTEMS)
25// flock() does not work in these OSes and produce warnings when we try to use
26# undef LOCK_EX
27# undef LOCK_NB
28#endif
29
30#include <sys/types.h> // kill
31#include <signal.h> // kill
32#include <unistd.h> // gethostname
33
34#if defined(Q_OS_MACOS)
35# include <libproc.h>
36#elif defined(Q_OS_LINUX)
37# include <unistd.h>
38# include <cstdio>
39#elif defined(Q_OS_HAIKU)
40# include <kernel/OS.h>
41#elif defined(Q_OS_BSD4) && !defined(QT_PLATFORM_UIKIT)
42# include <sys/cdefs.h>
43# include <sys/param.h>
44# include <sys/sysctl.h>
45# if !defined(Q_OS_NETBSD)
46# include <sys/user.h>
47# endif
48#endif
49
50#ifndef O_DIRECTORY
51# define O_DIRECTORY 0
52#endif
53#ifndef O_PATH
54# define O_PATH 0
55#endif
56
57QT_BEGIN_NAMESPACE
58
59using namespace Qt::StringLiterals;
60
61// ### merge into qt_safe_write?
62static qint64 qt_write_loop(int fd, const char *data, qint64 len)
63{
64 qint64 pos = 0;
65 while (pos < len) {
66 const qint64 ret = qt_safe_write(fd, data + pos, len - pos);
67 if (ret == -1) // e.g. partition full
68 return pos;
69 pos += ret;
70 }
71 return pos;
72}
73
74/*
75 * Details about file locking on Unix.
76 *
77 * There are three types of advisory locks on Unix systems:
78 * 1) POSIX process-wide locks using fcntl(F_SETLK)
79 * 2) BSD flock(2) system call
80 * 3) Linux-specific file descriptor locks using fcntl(F_OFD_SETLK)
81 * There's also a mandatory locking feature by POSIX, which is deprecated on
82 * Linux and users are advised not to use it.
83 *
84 * The first problem is that the POSIX API is braindead. POSIX.1-2008 says:
85 *
86 * All locks associated with a file for a given process shall be removed when
87 * a file descriptor for that file is closed by that process or the process
88 * holding that file descriptor terminates.
89 *
90 * The Linux manpage is clearer:
91 *
92 * * If a process closes _any_ file descriptor referring to a file, then all
93 * of the process's locks on that file are released, regardless of the file
94 * descriptor(s) on which the locks were obtained. This is bad: [...]
95 *
96 * * The threads in a process share locks. In other words, a multithreaded
97 * program can't use record locking to ensure that threads don't
98 * simultaneously access the same region of a file.
99 *
100 * So in order to use POSIX locks, we'd need a global mutex that stays locked
101 * while the QLockFile is locked. For that reason, Qt does not use POSIX
102 * advisory locks anymore.
103 *
104 * The next problem is that POSIX leaves undefined the relationship between
105 * locks with fcntl(), flock() and lockf(). In some systems (like the BSDs),
106 * all three use the same record set, while on others (like Linux) the locks
107 * are independent, except if locking over NFS mounts, in which case they're
108 * actually the same. Therefore, it's a very bad idea to mix them in the same
109 * process.
110 *
111 * We therefore use only flock(2), except on Android.
112 *
113 * Android Compatibility:
114 * Some versions of Android have known issues where flock does not function correctly.
115 * As a result, on Android, we use POSIX fcntl(F_SETLK) to handle file locking.
116 * fcntl is better integrated with Android’s underlying system, avoiding
117 * the limitations of flock.
118 *
119 * Race conditions and mitigations:
120 *
121 * 1) Concurrent creation of the lock file
122 * Creation of the lock file requires O_EXCL flag, which causes the OS to
123 * arbitrate. Only the thread that succeeded creating the lock file with this
124 * flag can claim to have locked.
125 *
126 * 2) Concurrent setNativeLocks()
127 * (Assuming an implementation that works, see main class docs for known
128 * limitations)
129 * This is applicable to tryLock_sys() and removeStaleLock() concurrency, as
130 * tryLock_sys() will only attempt to lock if open(O_EXCL) succeeded. The OS
131 * arbitrates and ensures only one thread at a time can lock the file's
132 * contents. QLockFile success implies the native lock either succeeded too or
133 * wasn't supported (definitely did not fail to lock).
134 *
135 * 3) Lock creation and unlink() race
136 * This is also applicable to tryLock_sys() and removeStaleLock() concurrency:
137 * following from the last case, removeStaleLock() may have natively-locked the
138 * file, unlinked it, then unlocked, all before tryLock_sys() reached
139 * setNativeLocks(). At this point, the lock file no longer exists in the
140 * directory but setNativeLocks() may yet succeed. This race is unavoidable, so
141 * we re-verify at the end whether the lock file is ours: if it is, then it has
142 * the lock and shouldn't get stolen.
143 */
144
145namespace {
146enum class NativeLocking {
147 UnknownError = -2,
148 NotSupported = -1,
149 Failed = 0,
150 Locked = 1,
151};
152}
153
154static NativeLocking setNativeLocks(int fd)
155{
156 int ret;
157#if defined(Q_OS_ANDROID)
158 struct flock fl;
159 fl.l_type = F_WRLCK;
160 fl.l_whence = SEEK_SET;
161 fl.l_start = 0;
162 fl.l_len = 0;
163 QT_EINTR_LOOP(ret, fcntl(fd, F_SETLK, &fl));
164#elif defined(LOCK_EX) && defined(LOCK_NB)
165 // applies to other threads, and other processes on a local fs
166 QT_EINTR_LOOP(ret, flock(fd, LOCK_EX | LOCK_NB));
167#else
168 Q_UNUSED(fd);
169 ret = -1;
170 errno = ENOTSUP;
171#endif
172
173 if (ret == 0)
174 return NativeLocking::Locked;
175
176 // failed to lock, but why?
177 switch (errno) {
178 case EWOULDBLOCK: // flock() documented to use this
179#if EAGAIN != EWOULDBLOCK
180 case EAGAIN: // F_SETLK documented to use this
181#endif
182 case EACCES: // F_SETLK documented to possibly use this too
183 return NativeLocking::Failed;
184
185 case ENOLCK:
186 // This can be both a resource error (lock table is full) and locking
187 // is not possible (e.g., rpc.lockd unable to lock). We err on the side
188 // of the permanence (more likely).
189 return NativeLocking::NotSupported;
190
191 case EINVAL:
192 case ENOTSUP:
193 case ENOSYS:
194 return NativeLocking::NotSupported;
195 }
196
197 return NativeLocking::UnknownError;
198}
199
200static QByteArrayView baseName(const QByteArray &fileName)
201{
202 // assert that the incoming string is null-terminated
203 Q_ASSERT(fileName.data()[fileName.size()] == '\0');
204
205 // find the last separator
206 qsizetype sep = fileName.lastIndexOf('/');
207 if (sep < 0)
208 return fileName; // relative path is *just* the file name
209 return QByteArrayView(fileName).sliced(sep + 1);
210}
211
212static bool releaseLockFile(int fileHandle, int dirfd, const QByteArray &fullFileName)
213{
214#ifdef AT_FDCWD
215 if (dirfd < 0)
216 dirfd = AT_FDCWD;
217#else
218 // fake it
219 auto unlinkat = [](int fd, const char *name, int /*flags*/) {
220 Q_ASSERT(fd < 0);
221 return unlink(name);
222 };
223#endif
224
225 QByteArrayView fileName = fullFileName;
226 if (dirfd >= 0)
227 fileName = baseName(fullFileName);
228
229 // first we try to unlink the file while it's locked
230 int ret = unlinkat(dirfd, fileName.data(), 0);
231 int savedErrno = ret == 0 ? 0 : errno;
232 qt_safe_close(fileHandle);
233
234 switch (savedErrno) {
235 // we have observed on some systems we can't unlink a file with an
236 // active lock, so we must close it first, then unlink
237 case EBUSY: // observed with some NFS servers
238 case ETXTBSY: // not observed, just defensive
239 case EPERM: // probably can't unlink() an open file (independent of locks)
240 ret = unlinkat(dirfd, fileName.data(), 0);
241 savedErrno = ret == 0 ? 0 : errno;
242 }
243
244 if (savedErrno == 0 || savedErrno == ENOENT || savedErrno == ENOTDIR)
245 return true; // file is gone
246
247 qWarning("Could not remove our own lock file %s: %ls (maybe permissions changed meanwhile?)",
248 fileName.cbegin(), qUtf16Printable(qt_error_string(savedErrno)));
249 // This is bad because other users of this lock file will now have to wait for the stale-lock-timeout...
250
251 return false;
252}
253
254// This function is designed to deal with race conditions coming from two (or
255// more) threads/processes accessing the lock file at the same time, either in
256// tryLock_sys() itself or removeStaleLock() - both called by tryLock().
257QLockFile::LockError QLockFilePrivate::tryLock_sys(const QLockFilePrivate::LockFileInfo &current)
258{
259 constexpr int OpenFlags = QT_OPEN_RDWR | QT_OPEN_CREAT | QT_OPEN_EXCL;
260 const QByteArray lockFileName = QFile::encodeName(fileName);
261 QUniqueFileDescriptorHandle dirfd, fd;
262
263#ifdef AT_FDCWD
264 QByteArrayView base = baseName(lockFileName);
265 {
266 // attempt to open the directory where the file name will be
267 constexpr int DirOpenFlags = QT_OPEN_RDONLY | O_DIRECTORY | O_PATH | O_CLOEXEC;
268 QByteArray dirName;
269 if (qsizetype dirNameLen = lockFileName.size() - base.size())
270 dirName = lockFileName.first(dirNameLen); // including slash
271 else
272 dirName = "."_ba;
273
274 dirfd.reset(qt_safe_open(dirName.constBegin(), DirOpenFlags));
275 if (dirfd.get() >= 0) {
276 int ret;
277 QT_EINTR_LOOP(ret, openat(dirfd.get(), base.begin(), OpenFlags | O_CLOEXEC, 0666));
278 fd.reset(ret); // may be -1
279 }
280 }
281#endif
282
283 if (dirfd.get() < 0)
284 fd.reset(qt_safe_open(lockFileName.constBegin(), OpenFlags, 0666));
285 if (fd.get() < 0) {
286 switch (errno) {
287 case EEXIST:
288 return QLockFile::LockFailedError;
289 case EACCES:
290 case EROFS:
291 return QLockFile::PermissionError;
292 default:
293 return QLockFile::UnknownError;
294 }
295 }
296 // Ensure nobody else can delete the file while we have it
297 if (NativeLocking r = setNativeLocks(fd.get()); r == NativeLocking::Failed) {
298 // someone holds this lock
299 return QLockFile::LockFailedError;
300 } else if (r == NativeLocking::UnknownError) {
301 const int errnoSaved = errno;
302 qWarning() << "setNativeLocks failed:" << qt_error_string(errnoSaved);
303 }
304
305 if (QByteArray fileData = current.asFileContents();
306 qt_write_loop(fd.get(), fileData.constData(), fileData.size()) < fileData.size()) {
307 releaseLockFile(fd.release(), dirfd.get(), lockFileName);
308 return QLockFile::UnknownError; // partition full
309 }
310
311 // Confirm the lock file is our file.
312 QT_STATBUF fromFs = {}, fromFd = {};
313#if defined(AT_FDCWD) && defined(QT_FSTATAT)
314 if (dirfd.get() >= 0 && QT_FSTATAT(dirfd.get(), base.begin(), &fromFs, 0) < 0) {
315 // probably ENOENT, meaning the file has disappeared
316 return QLockFile::LockFailedError;
317 }
318#endif
319 if (fromFs.st_dev == 0 && QT_STAT(lockFileName.constData(), &fromFs) < 0) {
320 // probably ENOENT, meaning the file has disappeared
321 return QLockFile::LockFailedError;
322 }
323 std::ignore = QT_FSTAT(fd.get(), &fromFd);
324 if (fromFd.st_dev != fromFs.st_dev || fromFd.st_ino != fromFs.st_ino) {
325 // lock file was replaced, stolen from under us
326 return QLockFile::LockFailedError;
327 }
328
329 // We hold the lock, continue.
330 fileHandle = fd.release();
331 dirHandle = dirfd.release();
332
333 // Sync to disk if possible. Ignore errors (e.g. not supported).
334#if defined(_POSIX_SYNCHRONIZED_IO) && _POSIX_SYNCHRONIZED_IO > 0
335 fdatasync(fileHandle);
336#else
337 fsync(fileHandle);
338#endif
339
340 return QLockFile::NoError;
341}
342
343bool QLockFilePrivate::removeStaleLock()
344{
345 const QByteArray lockFileName = QFile::encodeName(fileName);
346 const int fd = qt_safe_open(lockFileName.constData(), O_WRONLY, 0666);
347 if (fd < 0) // gone already?
348 return errno == ENOENT;
349
350 if (setNativeLocks(fd) != NativeLocking::Failed) {
351 // if we applied native locks, the file was stale; do delete it
352 return releaseLockFile(fd, -1, lockFileName);
353 }
354
355 // there was a lock, so it's not stale
356 qt_safe_close(fd);
357 return false;
358}
359
360bool QLockFilePrivate::isProcessRunning(qint64 pid, const QString &appname)
361{
362 if (::kill(pid_t(pid), 0) == -1 && errno == ESRCH)
363 return false; // PID doesn't exist anymore
364
365 const QString processName = processNameByPid(pid);
366 if (!processName.isEmpty()) {
367 QFileInfo fi(appname);
368 if (fi.isSymLink())
369 fi.setFile(fi.symLinkTarget());
370 if (processName != fi.fileName())
371 return false; // PID got reused by a different application.
372 }
373
374 return true;
375}
376
377QString QLockFilePrivate::processNameByPid(qint64 pid)
378{
379#if defined(Q_OS_MACOS)
380 char name[1024];
381 proc_name(pid, name, sizeof(name) / sizeof(char));
382 return QFile::decodeName(name);
383#elif defined(Q_OS_HARMONY)
384 // On OHOS, all Qt applications are started by the appspawn daemon via fork() + dlopen(),
385 // so /proc/{pid}/exe always resolves to "appspawn" rather than the actual application.
386 // For the current process, qAppName() is used instead, which is set by the OHOS QPA plugin
387 // to the loaded .so path (e.g. libMyApp.so). For external processes, /proc/{pid}/... is
388 // inaccessible due to sandboxing, so an empty string is returned; this causes
389 // isProcessRunning() to skip the name comparison and rely solely on PID existence.
390 // Note: this means PID reuse by an external process is not detected.
391 return pid == ::getpid() ? qAppName() : QString();
392#elif defined(Q_OS_LINUX)
393 if (!qt_haveLinuxProcfs())
394 return QString();
395
396 char exePath[64];
397 sprintf(exePath, "/proc/%lld/exe", pid);
398
399 QByteArray buf = qt_readlink(exePath);
400 if (buf.isEmpty()) {
401 // The pid is gone. Return some invalid process name to fail the test.
402 return QStringLiteral("/ERROR/");
403 }
404
405 // remove the " (deleted)" suffix, if any
406 static const char deleted[] = " (deleted)";
407 if (buf.endsWith(deleted))
408 buf.chop(strlen(deleted));
409
410 return QFileSystemEntry(buf, QFileSystemEntry::FromNativePath()).fileName();
411#elif defined(Q_OS_HAIKU)
412 thread_info info;
413 if (get_thread_info(pid, &info) != B_OK)
414 return QString();
415 return QFile::decodeName(info.name);
416#elif defined(Q_OS_BSD4) && !defined(QT_PLATFORM_UIKIT)
417# if defined(Q_OS_NETBSD)
418 struct kinfo_proc2 kp;
419 int mib[6] = { CTL_KERN, KERN_PROC2, KERN_PROC_PID, (int)pid, sizeof(struct kinfo_proc2), 1 };
420# elif defined(Q_OS_OPENBSD)
421 struct kinfo_proc kp;
422 int mib[6] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, (int)pid, sizeof(struct kinfo_proc), 1 };
423# else
424 struct kinfo_proc kp;
425 int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, (int)pid };
426# endif
427 size_t len = sizeof(kp);
428 u_int mib_len = sizeof(mib)/sizeof(u_int);
429
430 if (sysctl(mib, mib_len, &kp, &len, NULL, 0) < 0)
431 return QString();
432
433# if defined(Q_OS_OPENBSD) || defined(Q_OS_NETBSD)
434 if (kp.p_pid != pid)
435 return QString();
436 QString name = QFile::decodeName(kp.p_comm);
437# else
438 if (kp.ki_pid != pid)
439 return QString();
440 QString name = QFile::decodeName(kp.ki_comm);
441# endif
442 return name;
443#elif defined(Q_OS_QNX)
444 char exePath[PATH_MAX];
445 sprintf(exePath, "/proc/%lld/exefile", pid);
446
447 int fd = qt_safe_open(exePath, O_RDONLY);
448 if (fd == -1)
449 return QString();
450
451 QT_STATBUF sbuf;
452 if (QT_FSTAT(fd, &sbuf) == -1) {
453 qt_safe_close(fd);
454 return QString();
455 }
456
457 QByteArray buffer(sbuf.st_size, Qt::Uninitialized);
458 buffer.resize(qt_safe_read(fd, buffer.data(), sbuf.st_size - 1));
459 if (buffer.isEmpty()) {
460 // The pid is gone. Return some invalid process name to fail the test.
461 return QStringLiteral("/ERROR/");
462 }
463 return QFileSystemEntry(buffer, QFileSystemEntry::FromNativePath()).fileName();
464#else
465 Q_UNUSED(pid);
466 return QString();
467#endif
468}
469
470int QLockFilePrivate::openNewFileDescriptor(const QString &fileName)
471{
472 return QT_OPEN(fileName.toLocal8Bit().constData(), QT_OPEN_RDONLY);
473}
474
475void QLockFile::unlock()
476{
477 Q_D(QLockFile);
478 if (!d->isLocked)
479 return;
480
481 const QByteArray lockFileName = QFile::encodeName(d->fileName);
482 releaseLockFile(d->fileHandle, d->dirHandle, lockFileName);
483 if (d->dirHandle != -1)
484 close(d->dirHandle);
485 d->fileHandle = -1;
486 d->dirHandle = -1;
487 d->lockError = QLockFile::NoError;
488 d->isLocked = false;
489}
490
491QT_END_NAMESPACE
#define O_DIRECTORY
#define O_PATH
static NativeLocking setNativeLocks(int fd)
static qint64 qt_write_loop(int fd, const char *data, qint64 len)
static QByteArrayView baseName(const QByteArray &fileName)
static bool releaseLockFile(int fileHandle, int dirfd, const QByteArray &fullFileName)