toolkit/profile/nsProfileLock.cpp
author Gijs Kruitbosch <gijskruitbosch@gmail.com>
Fri, 28 Dec 2018 13:16:51 +0000
changeset 509151 9607199858999a81708f78931f9fbb7fdf672ec4
parent 505471 66eb1f485c1a3ea81372758bc92292c9428b17cd
child 511623 5f4630838d46dd81dadb13220a4af0da9e23a619
permissions -rw-r--r--
Bug 1514724 - actually insert pocket in the 'right' place in browser.xul to avoid migration issues in browser_pageActions.js , rs=bustage on a CLOSED TREE

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsProfileLock.h"
#include "nsCOMPtr.h"
#include "nsQueryObject.h"
#include "nsString.h"

#if defined(XP_WIN)
#include "ProfileUnlockerWin.h"
#include "nsAutoPtr.h"
#endif

#if defined(XP_MACOSX)
#include <Carbon/Carbon.h>
#include <CoreFoundation/CoreFoundation.h>
#endif

#ifdef XP_UNIX
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include "prnetdb.h"
#include "prsystem.h"
#include "prenv.h"
#include "mozilla/Printf.h"
#endif

// **********************************************************************
// class nsProfileLock
//
// This code was moved from profile/src/nsProfileAccess.
// **********************************************************************

#if defined(XP_UNIX)
static bool sDisableSignalHandling = false;
#endif

nsProfileLock::nsProfileLock()
    : mHaveLock(false),
      mReplacedLockTime(0)
#if defined(XP_WIN)
      ,
      mLockFileHandle(INVALID_HANDLE_VALUE)
#elif defined(XP_UNIX)
      ,
      mPidLockFileName(nullptr),
      mLockFileDesc(-1)
#endif
{
#if defined(XP_UNIX)
  next = prev = this;
  sDisableSignalHandling = PR_GetEnv("MOZ_DISABLE_SIG_HANDLER") ? true : false;
#endif
}

nsProfileLock::nsProfileLock(nsProfileLock &src) { *this = src; }

nsProfileLock &nsProfileLock::operator=(nsProfileLock &rhs) {
  Unlock();

  mHaveLock = rhs.mHaveLock;
  rhs.mHaveLock = false;

#if defined(XP_WIN)
  mLockFileHandle = rhs.mLockFileHandle;
  rhs.mLockFileHandle = INVALID_HANDLE_VALUE;
#elif defined(XP_UNIX)
  mLockFileDesc = rhs.mLockFileDesc;
  rhs.mLockFileDesc = -1;
  mPidLockFileName = rhs.mPidLockFileName;
  rhs.mPidLockFileName = nullptr;
  if (mPidLockFileName) {
    // rhs had a symlink lock, therefore it was on the list.
    PR_REMOVE_LINK(&rhs);
    PR_APPEND_LINK(this, &mPidLockList);
  }
#endif

  return *this;
}

nsProfileLock::~nsProfileLock() { Unlock(); }

#if defined(XP_UNIX)

static int setupPidLockCleanup;

PRCList nsProfileLock::mPidLockList =
    PR_INIT_STATIC_CLIST(&nsProfileLock::mPidLockList);

void nsProfileLock::RemovePidLockFiles(bool aFatalSignal) {
  while (!PR_CLIST_IS_EMPTY(&mPidLockList)) {
    nsProfileLock *lock = static_cast<nsProfileLock *>(mPidLockList.next);
    lock->Unlock(aFatalSignal);
  }
}

static struct sigaction SIGHUP_oldact;
static struct sigaction SIGINT_oldact;
static struct sigaction SIGQUIT_oldact;
static struct sigaction SIGILL_oldact;
static struct sigaction SIGABRT_oldact;
static struct sigaction SIGSEGV_oldact;
static struct sigaction SIGTERM_oldact;

void nsProfileLock::FatalSignalHandler(int signo
#ifdef SA_SIGINFO
                                       ,
                                       siginfo_t *info, void *context
#endif
) {
  // Remove any locks still held.
  RemovePidLockFiles(true);

  // Chain to the old handler, which may exit.
  struct sigaction *oldact = nullptr;

  switch (signo) {
    case SIGHUP:
      oldact = &SIGHUP_oldact;
      break;
    case SIGINT:
      oldact = &SIGINT_oldact;
      break;
    case SIGQUIT:
      oldact = &SIGQUIT_oldact;
      break;
    case SIGILL:
      oldact = &SIGILL_oldact;
      break;
    case SIGABRT:
      oldact = &SIGABRT_oldact;
      break;
    case SIGSEGV:
      oldact = &SIGSEGV_oldact;
      break;
    case SIGTERM:
      oldact = &SIGTERM_oldact;
      break;
    default:
      MOZ_ASSERT_UNREACHABLE("bad signo");
      break;
  }

  if (oldact) {
    if (oldact->sa_handler == SIG_DFL) {
      // Make sure the default sig handler is executed
      // We need it to get Mozilla to dump core.
      sigaction(signo, oldact, nullptr);

      // Now that we've restored the default handler, unmask the
      // signal and invoke it.

      sigset_t unblock_sigs;
      sigemptyset(&unblock_sigs);
      sigaddset(&unblock_sigs, signo);

      sigprocmask(SIG_UNBLOCK, &unblock_sigs, nullptr);

      raise(signo);
    }
#ifdef SA_SIGINFO
    else if (oldact->sa_sigaction &&
             (oldact->sa_flags & SA_SIGINFO) == SA_SIGINFO) {
      oldact->sa_sigaction(signo, info, context);
    }
#endif
    else if (oldact->sa_handler && oldact->sa_handler != SIG_IGN) {
      oldact->sa_handler(signo);
    }
  }

  // Backstop exit call, just in case.
  _exit(signo);
}

nsresult nsProfileLock::LockWithFcntl(nsIFile *aLockFile) {
  nsresult rv = NS_OK;

  nsAutoCString lockFilePath;
  rv = aLockFile->GetNativePath(lockFilePath);
  if (NS_FAILED(rv)) {
    NS_ERROR("Could not get native path");
    return rv;
  }

  aLockFile->GetLastModifiedTime(&mReplacedLockTime);

  mLockFileDesc = open(lockFilePath.get(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
  if (mLockFileDesc != -1) {
    struct flock lock;
    lock.l_start = 0;
    lock.l_len = 0;  // len = 0 means entire file
    lock.l_type = F_WRLCK;
    lock.l_whence = SEEK_SET;

    // If fcntl(F_GETLK) fails then the server does not support/allow fcntl(),
    // return failure rather than access denied in this case so we fallback
    // to using a symlink lock, bug 303633.
    struct flock testlock = lock;
    if (fcntl(mLockFileDesc, F_GETLK, &testlock) == -1) {
      close(mLockFileDesc);
      mLockFileDesc = -1;
      rv = NS_ERROR_FAILURE;
    } else if (fcntl(mLockFileDesc, F_SETLK, &lock) == -1) {
      close(mLockFileDesc);
      mLockFileDesc = -1;

      // With OS X, on NFS, errno == ENOTSUP
      // XXX Check for that and return specific rv for it?
#ifdef DEBUG
      printf("fcntl(F_SETLK) failed. errno = %d\n", errno);
#endif
      if (errno == EAGAIN || errno == EACCES)
        rv = NS_ERROR_FILE_ACCESS_DENIED;
      else
        rv = NS_ERROR_FAILURE;
    }
  } else {
    NS_ERROR("Failed to open lock file.");
    rv = NS_ERROR_FAILURE;
  }
  return rv;
}

static bool IsSymlinkStaleLock(struct in_addr *aAddr, const char *aFileName,
                               bool aHaveFcntlLock) {
  // the link exists; see if it's from this machine, and if
  // so if the process is still active
  char buf[1024];
  int len = readlink(aFileName, buf, sizeof buf - 1);
  if (len > 0) {
    buf[len] = '\0';
    char *colon = strchr(buf, ':');
    if (colon) {
      *colon++ = '\0';
      unsigned long addr = inet_addr(buf);
      if (addr != (unsigned long)-1) {
        if (colon[0] == '+' && aHaveFcntlLock) {
          // This lock was placed by a Firefox build which would have
          // taken the fnctl lock, and we've already taken the fcntl lock,
          // so the process that created this obsolete lock must be gone
          return true;
        }

        char *after = nullptr;
        pid_t pid = strtol(colon, &after, 0);
        if (pid != 0 && *after == '\0') {
          if (addr != aAddr->s_addr) {
            // Remote lock: give up even if stuck.
            return false;
          }

          // kill(pid,0) is a neat trick to check if a
          // process exists
          if (kill(pid, 0) == 0 || errno != ESRCH) {
            // Local process appears to be alive, ass-u-me it
            // is another Mozilla instance, or a compatible
            // derivative, that's currently using the profile.
            // XXX need an "are you Mozilla?" protocol
            return false;
          }
        }
      }
    }
  }
  return true;
}

nsresult nsProfileLock::LockWithSymlink(nsIFile *aLockFile,
                                        bool aHaveFcntlLock) {
  nsresult rv;
  nsAutoCString lockFilePath;
  rv = aLockFile->GetNativePath(lockFilePath);
  if (NS_FAILED(rv)) {
    NS_ERROR("Could not get native path");
    return rv;
  }

  // don't replace an existing lock time if fcntl already got one
  if (!mReplacedLockTime)
    aLockFile->GetLastModifiedTimeOfLink(&mReplacedLockTime);

  struct in_addr inaddr;
  inaddr.s_addr = htonl(INADDR_LOOPBACK);

  char hostname[256];
  PRStatus status = PR_GetSystemInfo(PR_SI_HOSTNAME, hostname, sizeof hostname);
  if (status == PR_SUCCESS) {
    char netdbbuf[PR_NETDB_BUF_SIZE];
    PRHostEnt hostent;
    status = PR_GetHostByName(hostname, netdbbuf, sizeof netdbbuf, &hostent);
    if (status == PR_SUCCESS) memcpy(&inaddr, hostent.h_addr, sizeof inaddr);
  }

  mozilla::SmprintfPointer signature =
      mozilla::Smprintf("%s:%s%lu", inet_ntoa(inaddr),
                        aHaveFcntlLock ? "+" : "", (unsigned long)getpid());
  const char *fileName = lockFilePath.get();
  int symlink_rv, symlink_errno = 0, tries = 0;

  // use ns4.x-compatible symlinks if the FS supports them
  while ((symlink_rv = symlink(signature.get(), fileName)) < 0) {
    symlink_errno = errno;
    if (symlink_errno != EEXIST) break;

    if (!IsSymlinkStaleLock(&inaddr, fileName, aHaveFcntlLock)) break;

    // Lock seems to be bogus: try to claim it.  Give up after a large
    // number of attempts (100 comes from the 4.x codebase).
    (void)unlink(fileName);
    if (++tries > 100) break;
  }

  if (symlink_rv == 0) {
    // We exclusively created the symlink: record its name for eventual
    // unlock-via-unlink.
    rv = NS_OK;
    mPidLockFileName = strdup(fileName);
    if (mPidLockFileName) {
      PR_APPEND_LINK(this, &mPidLockList);
      if (!setupPidLockCleanup++) {
        // Clean up on normal termination.
        // This instanciates a dummy class, and will trigger the class
        // destructor when libxul is unloaded. This is equivalent to atexit(),
        // but gracefully handles dlclose().
        static RemovePidLockFilesExiting r;

        // Clean up on abnormal termination, using POSIX sigaction.
        // Don't arm a handler if the signal is being ignored, e.g.,
        // because mozilla is run via nohup.
        if (!sDisableSignalHandling) {
          struct sigaction act, oldact;
#ifdef SA_SIGINFO
          act.sa_sigaction = FatalSignalHandler;
          act.sa_flags = SA_SIGINFO | SA_ONSTACK;
#else
          act.sa_handler = FatalSignalHandler;
#endif
          sigfillset(&act.sa_mask);

#define CATCH_SIGNAL(signame)                      \
  PR_BEGIN_MACRO                                   \
  if (sigaction(signame, nullptr, &oldact) == 0 && \
      oldact.sa_handler != SIG_IGN) {              \
    sigaction(signame, &act, &signame##_oldact);   \
  }                                                \
  PR_END_MACRO

          CATCH_SIGNAL(SIGHUP);
          CATCH_SIGNAL(SIGINT);
          CATCH_SIGNAL(SIGQUIT);
          CATCH_SIGNAL(SIGILL);
          CATCH_SIGNAL(SIGABRT);
          CATCH_SIGNAL(SIGSEGV);
          CATCH_SIGNAL(SIGTERM);

#undef CATCH_SIGNAL
        }
      }
    }
  } else if (symlink_errno == EEXIST)
    rv = NS_ERROR_FILE_ACCESS_DENIED;
  else {
#ifdef DEBUG
    printf("symlink() failed. errno = %d\n", errno);
#endif
    rv = NS_ERROR_FAILURE;
  }
  return rv;
}
#endif /* XP_UNIX */

nsresult nsProfileLock::GetReplacedLockTime(PRTime *aResult) {
  *aResult = mReplacedLockTime;
  return NS_OK;
}

nsresult nsProfileLock::Lock(nsIFile *aProfileDir,
                             nsIProfileUnlocker **aUnlocker) {
#if defined(XP_MACOSX)
  NS_NAMED_LITERAL_STRING(LOCKFILE_NAME, ".parentlock");
  NS_NAMED_LITERAL_STRING(OLD_LOCKFILE_NAME, "parent.lock");
#elif defined(XP_UNIX)
  NS_NAMED_LITERAL_STRING(OLD_LOCKFILE_NAME, "lock");
  NS_NAMED_LITERAL_STRING(LOCKFILE_NAME, ".parentlock");
#else
  NS_NAMED_LITERAL_STRING(LOCKFILE_NAME, "parent.lock");
#endif

  nsresult rv;
  if (aUnlocker) *aUnlocker = nullptr;

  NS_ENSURE_STATE(!mHaveLock);

  bool isDir;
  rv = aProfileDir->IsDirectory(&isDir);
  if (NS_FAILED(rv)) return rv;
  if (!isDir) return NS_ERROR_FILE_NOT_DIRECTORY;

  nsCOMPtr<nsIFile> lockFile;
  rv = aProfileDir->Clone(getter_AddRefs(lockFile));
  if (NS_FAILED(rv)) return rv;

  rv = lockFile->Append(LOCKFILE_NAME);
  if (NS_FAILED(rv)) return rv;

  // Remember the name we're using so we can clean up
  rv = lockFile->Clone(getter_AddRefs(mLockFile));
  if (NS_FAILED(rv)) return rv;

#if defined(XP_MACOSX)
  // First, try locking using fcntl. It is more reliable on
  // a local machine, but may not be supported by an NFS server.

  rv = LockWithFcntl(lockFile);
  if (NS_FAILED(rv) && (rv != NS_ERROR_FILE_ACCESS_DENIED)) {
    // If that failed for any reason other than NS_ERROR_FILE_ACCESS_DENIED,
    // assume we tried an NFS that does not support it. Now, try with symlink.
    rv = LockWithSymlink(lockFile, false);
  }

  if (NS_SUCCEEDED(rv)) {
    // Check for the old-style lock used by pre-mozilla 1.3 builds.
    // Those builds used an earlier check to prevent the application
    // from launching if another instance was already running. Because
    // of that, we don't need to create an old-style lock as well.
    struct LockProcessInfo {
      ProcessSerialNumber psn;
      unsigned long launchDate;
    };

    PRFileDesc *fd = nullptr;
    int32_t ioBytes;
    ProcessInfoRec processInfo;
    LockProcessInfo lockProcessInfo;

    rv = lockFile->SetLeafName(OLD_LOCKFILE_NAME);
    if (NS_FAILED(rv)) return rv;
    rv = lockFile->OpenNSPRFileDesc(PR_RDONLY, 0, &fd);
    if (NS_SUCCEEDED(rv)) {
      ioBytes = PR_Read(fd, &lockProcessInfo, sizeof(LockProcessInfo));
      PR_Close(fd);

      if (ioBytes == sizeof(LockProcessInfo)) {
#ifdef __LP64__
        processInfo.processAppRef = nullptr;
#else
        processInfo.processAppSpec = nullptr;
#endif
        processInfo.processName = nullptr;
        processInfo.processInfoLength = sizeof(ProcessInfoRec);
        if (::GetProcessInformation(&lockProcessInfo.psn, &processInfo) ==
                noErr &&
            processInfo.processLaunchDate == lockProcessInfo.launchDate) {
          return NS_ERROR_FILE_ACCESS_DENIED;
        }
      } else {
        NS_WARNING("Could not read lock file - ignoring lock");
      }
    }
    rv = NS_OK;  // Don't propagate error from OpenNSPRFileDesc.
  }
#elif defined(XP_UNIX)
  // Get the old lockfile name
  nsCOMPtr<nsIFile> oldLockFile;
  rv = aProfileDir->Clone(getter_AddRefs(oldLockFile));
  if (NS_FAILED(rv)) return rv;
  rv = oldLockFile->Append(OLD_LOCKFILE_NAME);
  if (NS_FAILED(rv)) return rv;

  // First, try locking using fcntl. It is more reliable on
  // a local machine, but may not be supported by an NFS server.
  rv = LockWithFcntl(lockFile);
  if (NS_SUCCEEDED(rv)) {
    // Check to see whether there is a symlink lock held by an older
    // Firefox build, and also place our own symlink lock --- but
    // mark it "obsolete" so that other newer builds can break the lock
    // if they obtain the fcntl lock
    rv = LockWithSymlink(oldLockFile, true);

    // If the symlink failed for some reason other than it already
    // exists, then something went wrong e.g. the file system
    // doesn't support symlinks, or we don't have permission to
    // create a symlink there.  In such cases we should just
    // continue because it's unlikely there is an old build
    // running with a symlink there and we've already successfully
    // placed a fcntl lock.
    if (rv != NS_ERROR_FILE_ACCESS_DENIED) rv = NS_OK;
  } else if (rv != NS_ERROR_FILE_ACCESS_DENIED) {
    // If that failed for any reason other than NS_ERROR_FILE_ACCESS_DENIED,
    // assume we tried an NFS that does not support it. Now, try with symlink
    // using the old symlink path
    rv = LockWithSymlink(oldLockFile, false);
  }

#elif defined(XP_WIN)
  nsAutoString filePath;
  rv = lockFile->GetPath(filePath);
  if (NS_FAILED(rv)) return rv;

  lockFile->GetLastModifiedTime(&mReplacedLockTime);

  // always create the profile lock and never delete it so we can use its
  // modification timestamp to detect startup crashes
  mLockFileHandle = CreateFileW(filePath.get(), GENERIC_READ | GENERIC_WRITE,
                                0,  // no sharing - of course
                                nullptr, CREATE_ALWAYS, 0, nullptr);
  if (mLockFileHandle == INVALID_HANDLE_VALUE) {
    if (aUnlocker) {
      RefPtr<mozilla::ProfileUnlockerWin> unlocker(
          new mozilla::ProfileUnlockerWin(filePath));
      if (NS_SUCCEEDED(unlocker->Init())) {
        nsCOMPtr<nsIProfileUnlocker> unlockerInterface(
            do_QueryObject(unlocker));
        unlockerInterface.forget(aUnlocker);
      }
    }
    return NS_ERROR_FILE_ACCESS_DENIED;
  }
#endif

  if (NS_SUCCEEDED(rv)) mHaveLock = true;

  return rv;
}

nsresult nsProfileLock::Unlock(bool aFatalSignal) {
  nsresult rv = NS_OK;

  if (mHaveLock) {
#if defined(XP_WIN)
    if (mLockFileHandle != INVALID_HANDLE_VALUE) {
      CloseHandle(mLockFileHandle);
      mLockFileHandle = INVALID_HANDLE_VALUE;
    }
#elif defined(XP_UNIX)
    if (mPidLockFileName) {
      PR_REMOVE_LINK(this);
      (void)unlink(mPidLockFileName);

      // Only free mPidLockFileName if we're not in the fatal signal
      // handler.  The problem is that a call to free() might be the
      // cause of this fatal signal.  If so, calling free() might cause
      // us to wait on the malloc implementation's lock.  We're already
      // holding this lock, so we'll deadlock. See bug 522332.
      if (!aFatalSignal) free(mPidLockFileName);
      mPidLockFileName = nullptr;
    }
    if (mLockFileDesc != -1) {
      close(mLockFileDesc);
      mLockFileDesc = -1;
      // Don't remove it
    }
#endif

    mHaveLock = false;
  }

  return rv;
}

nsresult nsProfileLock::Cleanup() {
  if (mLockFile) {
    return mLockFile->Remove(false);
  }

  return NS_OK;
}