/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 "nsDumpUtils.h" #include "nsDirectoryServiceDefs.h" #include "nsDirectoryServiceUtils.h" #include #include "prenv.h" #include "mozilla/Services.h" #include "nsIObserverService.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/Unused.h" #include "SpecialSystemDirectory.h" #ifdef XP_UNIX // { # include "mozilla/Preferences.h" # include # include # include using namespace mozilla; /* * The following code supports triggering a registered callback upon * receiving a specific signal. * * Take about:memory for example, we register * 1. doGCCCDump for doMemoryReport * 2. doMemoryReport for sDumpAboutMemorySignum(SIGRTMIN) * and sDumpAboutMemoryAfterMMUSignum(SIGRTMIN+1). * * When we receive one of these signals, we write the signal number to a pipe. * The IO thread then notices that the pipe has been written to, and kicks off * the appropriate task on the main thread. * * This scheme is similar to using signalfd(), except it's portable and it * doesn't require the use of sigprocmask, which is problematic because it * masks signals received by child processes. * * In theory, we could use Chromium's MessageLoopForIO::CatchSignal() for this. * But that uses libevent, which does not handle the realtime signals (bug * 794074). */ // This is the write-end of a pipe that we use to notice when a // specific signal occurs. static Atomic sDumpPipeWriteFd(-1); const char FifoWatcher::kPrefName[] = "memory_info_dumper.watch_fifo.enabled"; static void DumpSignalHandler(int aSignum) { // This is a signal handler, so everything in here needs to be // async-signal-safe. Be careful! if (sDumpPipeWriteFd != -1) { uint8_t signum = static_cast(aSignum); Unused << write(sDumpPipeWriteFd, &signum, sizeof(signum)); } } NS_IMPL_ISUPPORTS(FdWatcher, nsIObserver); void FdWatcher::Init() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr os = services::GetObserverService(); os->AddObserver(this, "xpcom-shutdown", /* ownsWeak = */ false); XRE_GetIOMessageLoop()->PostTask(NewRunnableMethod( "FdWatcher::StartWatching", this, &FdWatcher::StartWatching)); } // Implementations may call this function multiple times if they ensure that // it's safe to call OpenFd() multiple times and they call StopWatching() // first. void FdWatcher::StartWatching() { MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); MOZ_ASSERT(mFd == -1); mFd = OpenFd(); if (mFd == -1) { LOG("FdWatcher: OpenFd failed."); return; } MessageLoopForIO::current()->WatchFileDescriptor(mFd, /* persistent = */ true, MessageLoopForIO::WATCH_READ, &mReadWatcher, this); } // Since implementations can call StartWatching() multiple times, they can of // course call StopWatching() multiple times. void FdWatcher::StopWatching() { MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); mReadWatcher.StopWatchingFileDescriptor(); if (mFd != -1) { close(mFd); mFd = -1; } } StaticRefPtr SignalPipeWatcher::sSingleton; /* static */ SignalPipeWatcher* SignalPipeWatcher::GetSingleton() { if (!sSingleton) { sSingleton = new SignalPipeWatcher(); sSingleton->Init(); ClearOnShutdown(&sSingleton); } return sSingleton; } void SignalPipeWatcher::RegisterCallback(uint8_t aSignal, PipeCallback aCallback) { MutexAutoLock lock(mSignalInfoLock); for (SignalInfoArray::index_type i = 0; i < mSignalInfo.Length(); ++i) { if (mSignalInfo[i].mSignal == aSignal) { LOG("Register Signal(%d) callback failed! (DUPLICATE)", aSignal); return; } } SignalInfo signalInfo = {aSignal, aCallback}; mSignalInfo.AppendElement(signalInfo); RegisterSignalHandler(signalInfo.mSignal); } void SignalPipeWatcher::RegisterSignalHandler(uint8_t aSignal) { struct sigaction action; memset(&action, 0, sizeof(action)); sigemptyset(&action.sa_mask); action.sa_handler = DumpSignalHandler; if (aSignal) { if (sigaction(aSignal, &action, nullptr)) { LOG("SignalPipeWatcher failed to register sig %d.", aSignal); } } else { MutexAutoLock lock(mSignalInfoLock); for (SignalInfoArray::index_type i = 0; i < mSignalInfo.Length(); i++) { if (sigaction(mSignalInfo[i].mSignal, &action, nullptr)) { LOG("SignalPipeWatcher failed to register signal(%d) " "dump signal handler.", mSignalInfo[i].mSignal); } } } } SignalPipeWatcher::~SignalPipeWatcher() { if (sDumpPipeWriteFd != -1) { StopWatching(); } } int SignalPipeWatcher::OpenFd() { MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); // Create a pipe. When we receive a signal in our signal handler, we'll // write the signum to the write-end of this pipe. int pipeFds[2]; if (pipe(pipeFds)) { LOG("SignalPipeWatcher failed to create pipe."); return -1; } // Close this pipe on calls to exec(). fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC); fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC); int readFd = pipeFds[0]; sDumpPipeWriteFd = pipeFds[1]; RegisterSignalHandler(); return readFd; } void SignalPipeWatcher::StopWatching() { MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); // Close sDumpPipeWriteFd /after/ setting the fd to -1. // Otherwise we have the (admittedly far-fetched) race where we // // 1) close sDumpPipeWriteFd // 2) open a new fd with the same number as sDumpPipeWriteFd // had. // 3) receive a signal, then write to the fd. int pipeWriteFd = sDumpPipeWriteFd.exchange(-1); close(pipeWriteFd); FdWatcher::StopWatching(); } void SignalPipeWatcher::OnFileCanReadWithoutBlocking(int aFd) { MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); uint8_t signum; ssize_t numReceived = read(aFd, &signum, sizeof(signum)); if (numReceived != sizeof(signum)) { LOG("Error reading from buffer in " "SignalPipeWatcher::OnFileCanReadWithoutBlocking."); return; } { MutexAutoLock lock(mSignalInfoLock); for (SignalInfoArray::index_type i = 0; i < mSignalInfo.Length(); i++) { if (signum == mSignalInfo[i].mSignal) { mSignalInfo[i].mCallback(signum); return; } } } LOG("SignalPipeWatcher got unexpected signum."); } StaticRefPtr FifoWatcher::sSingleton; /* static */ FifoWatcher* FifoWatcher::GetSingleton() { if (!sSingleton) { nsAutoCString dirPath; Preferences::GetCString("memory_info_dumper.watch_fifo.directory", dirPath); sSingleton = new FifoWatcher(dirPath); sSingleton->Init(); ClearOnShutdown(&sSingleton); } return sSingleton; } /* static */ bool FifoWatcher::MaybeCreate() { MOZ_ASSERT(NS_IsMainThread()); if (!XRE_IsParentProcess()) { // We want this to be main-process only, since two processes can't listen // to the same fifo. return false; } if (!Preferences::GetBool(kPrefName, false)) { LOG("Fifo watcher disabled via pref."); return false; } // The FifoWatcher is held alive by the observer service. if (!sSingleton) { GetSingleton(); } return true; } void FifoWatcher::RegisterCallback(const nsCString& aCommand, FifoCallback aCallback) { MutexAutoLock lock(mFifoInfoLock); for (FifoInfoArray::index_type i = 0; i < mFifoInfo.Length(); ++i) { if (mFifoInfo[i].mCommand.Equals(aCommand)) { LOG("Register command(%s) callback failed! (DUPLICATE)", aCommand.get()); return; } } FifoInfo aFifoInfo = {aCommand, aCallback}; mFifoInfo.AppendElement(aFifoInfo); } FifoWatcher::~FifoWatcher() = default; int FifoWatcher::OpenFd() { // If the memory_info_dumper.directory pref is specified, put the fifo // there. Otherwise, put it into the system's tmp directory. nsCOMPtr file; nsresult rv; if (mDirPath.Length() > 0) { rv = XRE_GetFileFromPath(mDirPath.get(), getter_AddRefs(file)); if (NS_FAILED(rv)) { LOG("FifoWatcher failed to open file \"%s\"", mDirPath.get()); return -1; } } else { rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(file)); if (NS_WARN_IF(NS_FAILED(rv))) { return -1; } } rv = file->AppendNative("debug_info_trigger"_ns); if (NS_WARN_IF(NS_FAILED(rv))) { return -1; } nsAutoCString path; rv = file->GetNativePath(path); if (NS_WARN_IF(NS_FAILED(rv))) { return -1; } // unlink might fail because the file doesn't exist, or for other reasons. // But we don't care it fails; any problems will be detected later, when we // try to mkfifo or open the file. if (unlink(path.get())) { LOG("FifoWatcher::OpenFifo unlink failed; errno=%d. " "Continuing despite error.", errno); } if (mkfifo(path.get(), 0766)) { LOG("FifoWatcher::OpenFifo mkfifo failed; errno=%d", errno); return -1; } # ifdef ANDROID // Android runs with a umask, so we need to chmod our fifo to make it // world-writable. chmod(path.get(), 0666); # endif int fd; do { // The fifo will block until someone else has written to it. In // particular, open() will block until someone else has opened it for // writing! We want open() to succeed and read() to block, so we open // with NONBLOCK and then fcntl that away. fd = open(path.get(), O_RDONLY | O_NONBLOCK); } while (fd == -1 && errno == EINTR); if (fd == -1) { LOG("FifoWatcher::OpenFifo open failed; errno=%d", errno); return -1; } // Make fd blocking now that we've opened it. if (fcntl(fd, F_SETFL, 0)) { close(fd); return -1; } return fd; } void FifoWatcher::OnFileCanReadWithoutBlocking(int aFd) { MOZ_ASSERT(XRE_GetIOMessageLoop() == MessageLoopForIO::current()); char buf[1024]; int nread; do { // sizeof(buf) - 1 to leave space for the null-terminator. nread = read(aFd, buf, sizeof(buf)); } while (nread == -1 && errno == EINTR); if (nread == -1) { // We want to avoid getting into a situation where // OnFileCanReadWithoutBlocking is called in an infinite loop, so when // something goes wrong, stop watching the fifo altogether. LOG("FifoWatcher hit an error (%d) and is quitting.", errno); StopWatching(); return; } if (nread == 0) { // If we get EOF, that means that the other side closed the fifo. We need // to close and re-open the fifo; if we don't, // OnFileCanWriteWithoutBlocking will be called in an infinite loop. LOG("FifoWatcher closing and re-opening fifo."); StopWatching(); StartWatching(); return; } nsAutoCString inputStr; inputStr.Append(buf, nread); // Trimming whitespace is important because if you do // |echo "foo" >> debug_info_trigger|, // it'll actually write "foo\n" to the fifo. inputStr.Trim("\b\t\r\n"); { MutexAutoLock lock(mFifoInfoLock); for (FifoInfoArray::index_type i = 0; i < mFifoInfo.Length(); i++) { const nsCString commandStr = mFifoInfo[i].mCommand; if (inputStr == commandStr.get()) { mFifoInfo[i].mCallback(inputStr); return; } } } LOG("Got unexpected value from fifo; ignoring it."); } #endif // XP_UNIX } // In Android case, this function will open a file named aFilename under // /data/local/tmp/"aFoldername". // Otherwise, it will open a file named aFilename under "NS_OS_TEMP_DIR". /* static */ nsresult nsDumpUtils::OpenTempFile(const nsACString& aFilename, nsIFile** aFile, const nsACString& aFoldername, Mode aMode) { #ifdef ANDROID // For Android, first try the downloads directory which is world-readable // rather than the temp directory which is not. if (!*aFile) { char* env = PR_GetEnv("DOWNLOADS_DIRECTORY"); if (env) { NS_NewNativeLocalFile(nsCString(env), /* followLinks = */ true, aFile); } } #endif nsresult rv; if (!*aFile) { if (NS_IsMainThread()) { // This allows tests to override, but isn't safe off-mainthread. rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, aFile); } else { rv = GetSpecialSystemDirectory(OS_TemporaryDirectory, aFile); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } #ifdef ANDROID // /data/local/tmp is a true tmp directory; anyone can create a file there, // but only the user which created the file can remove it. We want non-root // users to be able to remove these files, so we write them into a // subdirectory of the temp directory and chmod 777 that directory. if (!aFoldername.IsEmpty()) { rv = (*aFile)->AppendNative(aFoldername); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // It's OK if this fails; that probably just means that the directory // already exists. Unused << (*aFile)->Create(nsIFile::DIRECTORY_TYPE, 0777); nsAutoCString dirPath; rv = (*aFile)->GetNativePath(dirPath); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } while (chmod(dirPath.get(), 0777) == -1 && errno == EINTR) { } } #endif nsCOMPtr file(*aFile); rv = file->AppendNative(aFilename); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (aMode == CREATE_UNIQUE) { rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0666); } else { rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0666); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #ifdef ANDROID // Make this file world-read/writable; the permissions passed to the // CreateUnique call above are not sufficient on Android, which runs with a // umask. nsAutoCString path; rv = file->GetNativePath(path); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } while (chmod(path.get(), 0666) == -1 && errno == EINTR) { } #endif return NS_OK; }