fix(startup): move the single-instance lock to a private per-user file

The single-instance lock lived in /tmp, which is world-writable. Creating
it with mode 0666 there gets masked to 0644, so a second local user
cannot open it O_RDWR and is wrongly blocked from running the wallet at
all. A world-writable directory also lets another user pre-create or
symlink the lock path.

- The lock now lives in a per-user directory: $XDG_RUNTIME_DIR when it is
  an absolute directory the user owns, otherwise the per-user, per-variant
  config directory that startup has already created. Never /tmp.
- It is created mode 0600 with O_NOFOLLOW, and after opening we fstat it
  and refuse anything that is not a regular file we own, rejecting a
  planted symlink or a foreign-owned file.
- The lock file is no longer unlinked on release; flock drops on close,
  and leaving the file in place avoids an unlink/relock race (a stale
  file is harmless).

Tests cover acquiring and conflicting on the lock, the 0600 mode, that
release leaves the file in place, and that a symlinked lock path is
refused.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 20:57:21 -05:00
co-authored by Claude Opus 4.8
parent bf5cb39566
commit f47528de53
3 changed files with 73 additions and 14 deletions
+31 -14
View File
@@ -7,12 +7,14 @@
#include <cstdio>
#include "../util/logger.h"
#include "platform.h" // dragonx::util::Platform::getObsidianDragonDir()
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <cstring>
@@ -24,12 +26,7 @@ namespace util {
SingleInstance::SingleInstance(const std::string& app_name)
: app_name_(app_name)
{
#ifndef _WIN32
// Construct lock file path
const char* tmpdir = getenv("TMPDIR");
if (!tmpdir) tmpdir = "/tmp";
lock_path_ = std::string(tmpdir) + "/" + app_name_ + ".lock";
#endif
// POSIX lock path is computed in tryLock(), once the per-user dir exists.
}
SingleInstance::~SingleInstance()
@@ -68,14 +65,35 @@ bool SingleInstance::tryLock()
#else
// Linux/macOS: Use file lock (flock)
// O_CLOEXEC: children (xdg-open + the browser it starts, dragonxd, xmrig) must not inherit the lock;
// after a crash they'd keep the flock held and block relaunching the wallet.
lock_fd_ = open(lock_path_.c_str(), O_CREAT | O_RDWR | O_CLOEXEC, 0666);
// Per-user lock dir: $XDG_RUNTIME_DIR (if it is an absolute dir we own) else the per-user, per-variant
// config dir, which main() has already created before tryLock(). Never /tmp (world-writable: a 0666
// lock there is masked to 0644, so a second local user can't open it O_RDWR and is wrongly blocked).
std::string dir;
const char* xrd = getenv("XDG_RUNTIME_DIR");
if (xrd && xrd[0] == '/') {
struct stat ds;
if (stat(xrd, &ds) == 0 && S_ISDIR(ds.st_mode) && ds.st_uid == geteuid())
dir = xrd;
}
if (dir.empty()) dir = Platform::getObsidianDragonDir();
lock_path_ = dir + "/" + app_name_ + ".lock";
// O_CLOEXEC: children (xdg-open + the browser it starts, dragonxd, xmrig) must not inherit the lock, or
// after a crash they'd hold the flock and block relaunch. O_NOFOLLOW + the fstat below reject a planted
// symlink or a foreign-owned file. 0600: per-user.
lock_fd_ = open(lock_path_.c_str(), O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600);
if (lock_fd_ < 0) {
DEBUG_LOGF("Failed to open lock file %s: %s\n", lock_path_.c_str(), strerror(errno));
return false;
}
struct stat ls;
if (fstat(lock_fd_, &ls) != 0 || !S_ISREG(ls.st_mode) || ls.st_uid != geteuid()) {
DEBUG_LOGF("Lock file %s is not a regular file we own - refusing\n", lock_path_.c_str());
close(lock_fd_);
lock_fd_ = -1;
return false;
}
// Try non-blocking exclusive lock
if (flock(lock_fd_, LOCK_EX | LOCK_NB) != 0) {
@@ -124,10 +142,9 @@ void SingleInstance::unlock()
flock(lock_fd_, LOCK_UN);
close(lock_fd_);
lock_fd_ = -1;
// Remove the lock file
unlink(lock_path_.c_str());
DEBUG_LOGF("Single instance lock released (lock file removed)\n");
// The lock file is intentionally kept: flock releases on close, and not unlinking
// avoids an unlink/relock race.
DEBUG_LOGF("Single instance lock released\n");
}
#endif