From ab73817097b62dc84a2b941c1a0e4a102d0250a7 Mon Sep 17 00:00:00 2001 From: Zeno Endemann Date: Thu, 4 May 2023 14:57:08 +0200 Subject: [PATCH] Implement new perf elevated privilege system Instead of using a script that changes system configuration temporarily, just run perf as root. To properly synchronize with a launched app (that itself should not run as root), launch the app in a separate, initially stopped process, and use the control fifo feature of perf to properly synchronize with it. The control fifos are also needed to be able to stop sudo-perf, as one does not have permission to SIGINT it anymore. --- CMakeLists.txt | 1 - README.md | 7 +- scripts/CMakeLists.txt | 8 - scripts/elevate_perf_privileges.sh | 65 -------- src/CMakeLists.txt | 2 + src/initiallystoppedprocess.cpp | 131 ++++++++++++++++ src/initiallystoppedprocess.h | 47 ++++++ src/perfcontrolfifowrapper.cpp | 136 +++++++++++++++++ src/perfcontrolfifowrapper.h | 59 ++++++++ src/perfrecord.cpp | 205 +++++++++++--------------- src/perfrecord.h | 14 +- src/recordpage.cpp | 7 +- tests/integrationtests/CMakeLists.txt | 2 + tests/modeltests/CMakeLists.txt | 2 + 14 files changed, 483 insertions(+), 203 deletions(-) delete mode 100644 scripts/CMakeLists.txt delete mode 100755 scripts/elevate_perf_privileges.sh create mode 100644 src/initiallystoppedprocess.cpp create mode 100644 src/initiallystoppedprocess.h create mode 100644 src/perfcontrolfifowrapper.cpp create mode 100644 src/perfcontrolfifowrapper.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8871f73d..08685718 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,7 +144,6 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") endif() add_subdirectory(3rdparty) -add_subdirectory(scripts) include_directories(${CMAKE_CURRENT_BINARY_DIR}) add_subdirectory(src) diff --git a/README.md b/README.md index b43ff5ea..bea12358 100644 --- a/README.md +++ b/README.md @@ -407,12 +407,7 @@ Consider tweaking /proc/sys/kernel/perf_event_paranoid: 2 - Disallow kernel profiling for unpriv ``` -To workaround this limitation, hotspot can temporarily elevate the perf privileges. -This is achieved by applying -[these steps](https://superuser.com/questions/980632/run-perf-without-root-right), -bundled into [a script](scripts/elevate_perf_privileges.sh) that is run via `pkexec`, `kdesudo` or `kdesu`. -The resulting elevated privileges are also required for kernel tracing in general and Off-CPU profiling in -particular. +To workaround this limitation, hotspot can run perf itself with elevated privileges. ### Export File Format diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt deleted file mode 100644 index 2d58207a..00000000 --- a/scripts/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -configure_file( - elevate_perf_privileges.sh "${PROJECT_BINARY_DIR}/${KDE_INSTALL_LIBEXECDIR}/elevate_perf_privileges.sh" @ONLY -) - -install( - PROGRAMS elevate_perf_privileges.sh - DESTINATION ${KDE_INSTALL_LIBEXECDIR} -) diff --git a/scripts/elevate_perf_privileges.sh b/scripts/elevate_perf_privileges.sh deleted file mode 100755 index 85f30c08..00000000 --- a/scripts/elevate_perf_privileges.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/sh -# -# SPDX-FileCopyrightText: Milian Wolff -# SPDX-FileCopyrightText: 2016-2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com -# -# SPDX-License-Identifier: GPL-2.0-or-later -# - -if [ "$(id -u)" != "0" ]; then - echo "Error: This script must be run as root" - exit 1 -fi - -if [ ! -z "$1" ]; then - olduser=$(stat -c '%u' "$1") - chown "$(whoami)" "$1" - echo "rewriting to $1" - # redirect output to file, to enable parsing of output even when - # the graphical sudo helper like kdesudo isn't forwarding the text properly - $0 2>&1 | tee -a "$1" - chown "$olduser" "$1" - exit -fi - -echo "querying current privileges..." - -old_sysctl_state=$(sysctl kernel.kptr_restrict kernel.perf_event_paranoid | sed 's/ = /=/') -old_debug_permissions=$(stat -c "%a" /sys/kernel/debug) -old_tracing_permissions=$(stat -c "%a" /sys/kernel/debug/tracing) - -printPrivileges() { - sysctl kernel.kptr_restrict kernel.perf_event_paranoid - stat -c "%n %a" /sys/kernel/debug /sys/kernel/debug/tracing -} - -printPrivileges -echo - -# restore old privileges when exiting this script -cleanup() { - echo "restoring old privileges..." - sysctl -wq $old_sysctl_state - mount -o remount,mode=$old_debug_permissions /sys/kernel/debug - mount -o remount,mode=$old_tracing_permissions /sys/kernel/debug/tracing - printPrivileges -} -trap cleanup EXIT - -# handle term and int, such that the exit handler gets called (but only once) -# if we'd add INT and TERM to the cleanup trap above, it would get called twice -quit() { - echo "quitting..." -} -trap quit TERM INT - -echo "elevating privileges..." -sysctl -wq kernel.kptr_restrict=0 kernel.perf_event_paranoid=-1 -mount -o remount,mode=755 /sys/kernel/debug -mount -o remount,mode=755 /sys/kernel/debug/tracing -printPrivileges - -echo -echo "privileges elevated!" -read something 2> /dev/null -echo diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d4698b23..0b35c06b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -50,6 +50,8 @@ set(HOTSPOT_SRCS perfoutputwidgettext.cpp perfoutputwidgetkonsole.cpp costcontextmenu.cpp + initiallystoppedprocess.cpp + perfcontrolfifowrapper.cpp # ui files: mainwindow.ui aboutdialog.ui diff --git a/src/initiallystoppedprocess.cpp b/src/initiallystoppedprocess.cpp new file mode 100644 index 00000000..a673a754 --- /dev/null +++ b/src/initiallystoppedprocess.cpp @@ -0,0 +1,131 @@ +/* + SPDX-FileCopyrightText: Zeno Endemann + SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "initiallystoppedprocess.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { +Q_LOGGING_CATEGORY(initiallystoppedprocess, "hotspot.initiallystoppedprocess") + +void sendSignal(pid_t pid, int signal) +{ + if (kill(pid, signal) != 0) { + qCCritical(initiallystoppedprocess) + << "Failed to send signal" << signal << "to" << pid << ":" << std::strerror(errno); + } +} +} + +InitiallyStoppedProcess::~InitiallyStoppedProcess() +{ + kill(); +} + +bool InitiallyStoppedProcess::createProcessAndStop(const QString& exePath, const QStringList& exeOptions, + const QString& workingDirectory) +{ + kill(); + + // convert arguments and working dir into what the C API needs + const auto wd = workingDirectory.toLocal8Bit(); + + std::vector argsQBA; + std::vector argsRaw; + auto addArg = [&](const QString& arg) { + argsQBA.emplace_back(arg.toLocal8Bit()); + argsRaw.emplace_back(argsQBA.back().data()); + }; + + argsQBA.reserve(exeOptions.size() + 1); + argsRaw.reserve(exeOptions.size() + 2); + + addArg(exePath); + for (const auto& opt : exeOptions) + addArg(opt); + + argsRaw.emplace_back(nullptr); + + // fork + m_pid = fork(); + + if (m_pid == 0) { // inside child process + // change working dir + if (!wd.isEmpty() && chdir(wd.data()) != 0) { + qCCritical(initiallystoppedprocess) + << "Failed to change working directory to:" << wd.data() << std::strerror(errno); + } + + // stop self + if (raise(SIGSTOP) != 0) { + qCCritical(initiallystoppedprocess) << "Failed to raise SIGSTOP:" << std::strerror(errno); + } + + // exec + execvp(argsRaw[0], argsRaw.data()); + qCCritical(initiallystoppedprocess) << "Failed to exec" << argsRaw[0] << std::strerror(errno); + } else if (m_pid < 0) { + qCCritical(initiallystoppedprocess) << "Failed to fork:" << std::strerror(errno); + return false; + } + + return true; +} + +bool InitiallyStoppedProcess::continueStoppedProcess() +{ + if (m_pid <= 0) + return false; + + // wait for child to be stopped + + int wstatus; + if (waitpid(m_pid, &wstatus, WUNTRACED) == -1) { + qCWarning(initiallystoppedprocess()) << "Failed to wait on process:" << std::strerror(errno); + } + + if (!WIFSTOPPED(wstatus)) { + m_pid = -1; + return false; + } + + // continue + + sendSignal(m_pid, SIGCONT); + return true; +} + +void InitiallyStoppedProcess::terminate() +{ + if (m_pid > 0) { + sendSignal(m_pid, SIGTERM); + } +} + +void InitiallyStoppedProcess::kill() +{ + if (m_pid > 0) { + sendSignal(m_pid, SIGILL); + if (waitpid(m_pid, nullptr, 0) == -1) { + qCWarning(initiallystoppedprocess()) << "failed to wait on pid:" << m_pid << std::strerror(errno); + } + m_pid = -1; + } +} diff --git a/src/initiallystoppedprocess.h b/src/initiallystoppedprocess.h new file mode 100644 index 00000000..48a97e78 --- /dev/null +++ b/src/initiallystoppedprocess.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: Zeno Endemann + SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include + +class InitiallyStoppedProcess +{ + Q_DISABLE_COPY(InitiallyStoppedProcess) +public: + InitiallyStoppedProcess() = default; + ~InitiallyStoppedProcess(); + + /// @return the PID of the child process, or -1 if no process was started yet + pid_t processPID() const + { + return m_pid; + } + + /// this function stops any existing child process and then creates a new child process + /// and changes into @p workingDirectory. The process will be stopped immediately. + /// After receiving SIGCONT it will run @p exePath with @p exeOptions + /// @sa continueStoppedProcess + bool createProcessAndStop(const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory); + + /// wait for child process to be stopped and then continues its execution + /// @sa createProcessAndStop + bool continueStoppedProcess(); + + /// send SIGTERM to the child process + void terminate(); + + /// send SIGKILL to the child process + void kill(); + +private: + pid_t m_pid = -1; +}; diff --git a/src/perfcontrolfifowrapper.cpp b/src/perfcontrolfifowrapper.cpp new file mode 100644 index 00000000..59093c23 --- /dev/null +++ b/src/perfcontrolfifowrapper.cpp @@ -0,0 +1,136 @@ +/* + SPDX-FileCopyrightText: Zeno Endemann + SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "perfcontrolfifowrapper.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace { +Q_LOGGING_CATEGORY(perfcontrolfifowrapper, "hotspot.perfcontrolfifowrapper") + +QString randomString() +{ + return QUuid::createUuid().toString(QUuid::WithoutBraces); +} + +int createAndOpenFifo(const QString& name) +{ + const auto localName = name.toLocal8Bit(); + if (mkfifo(localName.constData(), 0600) != 0) { + qCCritical(perfcontrolfifowrapper) << "Cannot create fifo" << name << std::strerror(errno); + return -1; + } + + auto fd = open(localName.constData(), O_RDWR); + if (fd < 0) { + qCCritical(perfcontrolfifowrapper) << "Cannot open fifo" << name << std::strerror(errno); + return -1; + } + return fd; +} +} + +PerfControlFifoWrapper::~PerfControlFifoWrapper() +{ + close(); +} + +bool PerfControlFifoWrapper::open() +{ + close(); + + // QStandardPaths::RuntimeLocation may be empty -> fallback to TempLocation + auto fifoParentPath = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); + if (fifoParentPath.isEmpty()) + fifoParentPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + + const auto fifoBasePath = + QLatin1String("%1/hotspot-%2-%3-perf").arg(fifoParentPath, QString::number(getpid()), randomString()); + m_ctlFifoPath = fifoBasePath + QLatin1String("-control.fifo"); + m_ackFifoPath = fifoBasePath + QLatin1String("-ack.fifo"); + + // error is already handles by createAndOpenFifo + m_ctlFifoFd = createAndOpenFifo(m_ctlFifoPath); + if (m_ctlFifoFd < 0) + return false; + + m_ackFifoFd = createAndOpenFifo(m_ackFifoPath); + if (m_ackFifoFd < 0) + return false; + + return true; +} + +void PerfControlFifoWrapper::requestStart() +{ + if (m_ctlFifoFd < 0) { + emit noFIFO(); + return; + } + + m_ackReady = std::make_unique(m_ackFifoFd, QSocketNotifier::Read); + connect(m_ackReady.get(), &QSocketNotifier::activated, this, [this]() { + char buf[10]; + if (read(m_ackFifoFd, buf, sizeof(buf)) == -1) { + qCWarning(perfcontrolfifowrapper) + << "failed to read message from fifo:" << m_ctlFifoPath << std::strerror(errno); + } + emit started(); + m_ackReady->disconnect(this); + }); + + const char start_cmd[] = "enable\n"; + if (write(m_ctlFifoFd, start_cmd, sizeof(start_cmd) - 1) == -1) { + qCWarning(perfcontrolfifowrapper) + << "failed to write start message to fifo:" << m_ctlFifoPath << std::strerror(errno); + } +} + +void PerfControlFifoWrapper::requestStop() +{ + if (m_ctlFifoFd < 0) { + emit noFIFO(); + return; + } + const char stop_cmd[] = "stop\n"; + if (write(m_ctlFifoFd, stop_cmd, sizeof(stop_cmd) - 1) == -1) { + qCWarning(perfcontrolfifowrapper) + << "failed to write start message to fifo:" << m_ctlFifoPath << std::strerror(errno); + } +} + +void PerfControlFifoWrapper::close() +{ + if (m_ackReady) { + m_ackReady = nullptr; + } + if (m_ctlFifoFd >= 0) { + ::close(m_ctlFifoFd); + m_ctlFifoFd = -1; + } + if (m_ackFifoFd >= 0) { + ::close(m_ackFifoFd); + m_ackFifoFd = -1; + } + if (!m_ctlFifoPath.isEmpty()) { + QFile::remove(m_ctlFifoPath); + m_ctlFifoPath.clear(); + } + if (!m_ackFifoPath.isEmpty()) { + QFile::remove(m_ackFifoPath); + m_ackFifoPath.clear(); + } +} diff --git a/src/perfcontrolfifowrapper.h b/src/perfcontrolfifowrapper.h new file mode 100644 index 00000000..5a56e8d3 --- /dev/null +++ b/src/perfcontrolfifowrapper.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: Zeno Endemann + SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include + +class QSocketNotifier; + +/** + * Wrapper for the control and ack FIFOs for perf record. + * + * For more information, refer to `man perf record` and search for `--control=fifo:`. + */ +class PerfControlFifoWrapper : public QObject +{ + Q_OBJECT + +public: + using QObject::QObject; + ~PerfControlFifoWrapper(); + + bool isOpen() const + { + return m_ctlFifoFd >= 0; + } + QString controlFifoPath() const + { + return m_ctlFifoPath; + } + QString ackFifoPath() const + { + return m_ackFifoPath; + } + + bool open(); + void requestStart(); + void requestStop(); + void close(); + +signals: + void started(); + void noFIFO(); + +private: + std::unique_ptr m_ackReady; + QString m_ctlFifoPath; + QString m_ackFifoPath; + int m_ctlFifoFd = -1; + int m_ackFifoFd = -1; +}; diff --git a/src/perfrecord.cpp b/src/perfrecord.cpp index 8e9e939e..b3c46019 100644 --- a/src/perfrecord.cpp +++ b/src/perfrecord.cpp @@ -28,20 +28,35 @@ #include #endif -#include "util.h" - #include #include #include +namespace { +void createOutputFile(const QString& outputPath) +{ + // elevated perf will obviously create a root-owned output by default, but testing revealed that + // perf will write into a pre-existing file if it is empty (without changing ownership) + const QString bakPath(outputPath + QStringLiteral(".old")); + // QFile::rename does not overwrite files, so we need to remove it manually + QFile::remove(bakPath); + QFile::rename(outputPath, bakPath); + QFile(outputPath).open(QIODevice::WriteOnly); +} +} + PerfRecord::PerfRecord(QObject* parent) : QObject(parent) , m_perfRecordProcess(nullptr) - , m_elevatePrivilegesProcess(nullptr) , m_outputPath() , m_userTerminated(false) { + connect(&m_perfControlFifo, &PerfControlFifoWrapper::started, this, + [this]() { m_targetProcessForPrivilegedPerf.continueStoppedProcess(); }); + + connect(&m_perfControlFifo, &PerfControlFifoWrapper::noFIFO, this, + [this] { emit recordingFailed(QStringLiteral("Failed to start process, broken control FIFO")); }); } PerfRecord::~PerfRecord() @@ -105,102 +120,13 @@ static bool privsAlreadyElevated() return isElevated; } -void PerfRecord::startRecording(bool elevatePrivileges, const QStringList& perfOptions, const QString& outputPath, - const QStringList& recordOptions, const QString& workingDirectory) -{ - if (canElevatePrivileges() && elevatePrivileges && geteuid() != 0 && !privsAlreadyElevated()) { - // elevate privileges temporarily as root - // use pkexec/kdesudo to start the elevate_perf_privileges.sh script - // then parse its output and once we get the "waiting..." line the privileges got elevated - // in that case, we can continue to start perf and quit the elevate_perf_privileges.sh script - // once perf has started - const auto sudoBinary = sudoUtil(); - if (sudoBinary.isEmpty()) { - emit recordingFailed(tr("No sudo utility found. Please install pkexec, kdesudo or kdesu.")); - return; - } - - const auto elevateScript = Util::findLibexecBinary(QStringLiteral("elevate_perf_privileges.sh")); - if (elevateScript.isEmpty()) { - emit recordingFailed(tr("Failed to find `elevate_perf_privileges.sh` script.")); - return; - } - - auto options = sudoOptions(sudoBinary); - options.append(elevateScript); - - if (m_elevatePrivilegesProcess) { - m_elevatePrivilegesProcess->kill(); - m_elevatePrivilegesProcess->deleteLater(); - } - m_elevatePrivilegesProcess = new QProcess(this); - m_elevatePrivilegesProcess->setProcessChannelMode(QProcess::ForwardedChannels); - - // I/O redirection of client scripts launched by kdesu & friends doesn't work, i.e. no data can be read... - // so instead we use a temporary file and parse its contents via a polling timer :-/ - auto* outputFile = new QTemporaryFile(m_elevatePrivilegesProcess); - outputFile->open(); - options.append(outputFile->fileName()); - - connect(this, &PerfRecord::recordingStarted, m_elevatePrivilegesProcess.data(), [this] { - QTimer::singleShot(1000, m_elevatePrivilegesProcess, [this]() { - emit recordingOutput(QStringLiteral("\nrestoring privileges...\n")); - m_elevatePrivilegesProcess->terminate(); - }); - }); - connect(m_elevatePrivilegesProcess.data(), &QProcess::errorOccurred, m_elevatePrivilegesProcess.data(), - [this](QProcess::ProcessError /*error*/) { - if (!m_perfRecordProcess) { - emit recordingFailed( - tr("Failed to elevate privileges: %1").arg(m_elevatePrivilegesProcess->errorString())); - m_elevatePrivilegesProcess->deleteLater(); - } - }); - // poll the file for new input, readyRead isn't being emitted by QFile (cf. docs) - auto* readTimer = new QTimer(outputFile); - auto readSlot = [this, outputFile, perfOptions, outputPath, recordOptions, workingDirectory]() { - const auto data = outputFile->readAll(); - if (data.isEmpty()) { - return; - } - - if (data.contains("\nprivileges elevated!\n")) { - emit recordingOutput(QString::fromUtf8(data)); - emit recordingOutput(QStringLiteral("\n")); - startRecording(perfOptions, outputPath, recordOptions, workingDirectory); - } else if (data.contains("Error:")) { - emit recordingFailed(tr("Failed to elevate privileges: %1").arg(QString::fromUtf8(data))); - } else { - emit recordingOutput(QString::fromUtf8(data)); - } - }; - connect(readTimer, &QTimer::timeout, this, readSlot); - connect(m_elevatePrivilegesProcess.data(), &QProcess::started, readTimer, - [readTimer] { readTimer->start(250); }); - connect(m_elevatePrivilegesProcess.data(), - static_cast(&QProcess::finished), - m_elevatePrivilegesProcess.data(), [this, readSlot] { - // read remaining data - readSlot(); - // then delete the process - m_elevatePrivilegesProcess->deleteLater(); - - if (!m_perfRecordProcess) { - emit recordingFailed(tr("Failed to elevate privileges.")); - } - }); - - m_elevatePrivilegesProcess->start(sudoBinary, options); - } else { - startRecording(perfOptions, outputPath, recordOptions, workingDirectory); - } -} - -void PerfRecord::startRecording(const QStringList& perfOptions, const QString& outputPath, - const QStringList& recordOptions, const QString& workingDirectory) +bool PerfRecord::runPerf(bool elevatePrivileges, const QStringList& perfOptions, const QString& outputPath, + const QString& workingDirectory) { // Reset perf record process to avoid getting signals from old processes if (m_perfRecordProcess) { + m_perfControlFifo.requestStop(); + m_perfControlFifo.close(); m_perfRecordProcess->kill(); m_perfRecordProcess->deleteLater(); } @@ -212,15 +138,15 @@ void PerfRecord::startRecording(const QStringList& perfOptions, const QString& o QFileInfo folderInfo(folderPath); if (!folderInfo.exists()) { emit recordingFailed(tr("Folder '%1' does not exist.").arg(folderPath)); - return; + return false; } if (!folderInfo.isDir()) { emit recordingFailed(tr("'%1' is not a folder.").arg(folderPath)); - return; + return false; } if (!folderInfo.isWritable()) { emit recordingFailed(tr("Folder '%1' is not writable.").arg(folderPath)); - return; + return false; } connect(m_perfRecordProcess.data(), static_cast(&QProcess::finished), @@ -247,12 +173,16 @@ void PerfRecord::startRecording(const QStringList& perfOptions, const QString& o } }); + connect(m_perfRecordProcess.data(), &QProcess::started, this, + [this] { emit recordingStarted(m_perfRecordProcess->program(), m_perfRecordProcess->arguments()); }); + connect(m_perfRecordProcess.data(), &QProcess::readyRead, this, [this]() { QString output = QString::fromUtf8(m_perfRecordProcess->readAll()); emit recordingOutput(output); }); m_outputPath = outputPath; + m_userTerminated = false; if (!workingDirectory.isEmpty()) { m_perfRecordProcess->setWorkingDirectory(workingDirectory); @@ -260,13 +190,34 @@ void PerfRecord::startRecording(const QStringList& perfOptions, const QString& o QStringList perfCommand = {QStringLiteral("record"), QStringLiteral("-o"), m_outputPath}; perfCommand += perfOptions; - perfCommand += recordOptions; - connect(m_perfRecordProcess.data(), &QProcess::started, this, - [this] { emit recordingStarted(m_perfRecordProcess->program(), m_perfRecordProcess->arguments()); }); - m_perfRecordProcess->start(perfBinaryPath(), perfCommand); + if (elevatePrivileges) { + const auto sudoBinary = sudoUtil(); + if (sudoBinary.isEmpty()) { + emit recordingFailed(tr("No sudo utility found. Please install pkexec, kdesudo or kdesu.")); + return false; + } - m_userTerminated = false; + auto options = sudoOptions(sudoBinary); + options.append(perfBinaryPath()); + options += perfCommand; + + if (!m_perfControlFifo.open()) { + emit recordingFailed(tr("Failed to create perf control fifos.")); + return false; + } + options += + {QStringLiteral("--control"), + QStringLiteral("fifo:%1,%2").arg(m_perfControlFifo.controlFifoPath(), m_perfControlFifo.ackFifoPath())}; + + createOutputFile(outputPath); + + m_perfRecordProcess->start(sudoBinary, options); + } else { + m_perfRecordProcess->start(perfBinaryPath(), perfCommand); + } + + return true; } void PerfRecord::record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges, @@ -279,7 +230,7 @@ void PerfRecord::record(const QStringList& perfOptions, const QString& outputPat QStringList options = perfOptions; options += {QStringLiteral("--pid"), pids.join(QLatin1Char(','))}; - startRecording(elevatePrivileges, options, outputPath, {}); + runPerf(actuallyElevatePrivileges(elevatePrivileges), options, outputPath, {}); } void PerfRecord::record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges, @@ -304,23 +255,39 @@ void PerfRecord::record(const QStringList& perfOptions, const QString& outputPat return; } - QStringList recordOptions = {exeFileInfo.absoluteFilePath()}; - recordOptions += exeOptions; + QStringList options = perfOptions; + if (actuallyElevatePrivileges(elevatePrivileges)) { + if (!m_targetProcessForPrivilegedPerf.createProcessAndStop(exePath, exeOptions, workingDirectory)) { + emit recordingFailed(tr("Failed to prepare a stopped process for %1.").arg(exePath)); + return; + } + options += {QStringLiteral("--pid"), QString::number(m_targetProcessForPrivilegedPerf.processPID()), + QStringLiteral("-D"), QStringLiteral("-1")}; + if (!runPerf(true, options, outputPath, {})) { + m_targetProcessForPrivilegedPerf.kill(); + return; + } - startRecording(elevatePrivileges, perfOptions, outputPath, recordOptions, workingDirectory); + m_perfControlFifo.requestStart(); + } else { + options.append(exeFileInfo.absoluteFilePath()); + options += exeOptions; + runPerf(false, options, outputPath, workingDirectory); + } } void PerfRecord::recordSystem(const QStringList& perfOptions, const QString& outputPath) { auto options = perfOptions; options.append(QStringLiteral("--all-cpus")); - startRecording(true, options, outputPath, {}); + runPerf(actuallyElevatePrivileges(true), options, outputPath, {}); } const QString PerfRecord::perfCommand() { if (m_perfRecordProcess) { - return m_perfRecordProcess->program() + QLatin1Char(' ') + m_perfRecordProcess->arguments().join(QLatin1Char(' ')); + return m_perfRecordProcess->program() + QLatin1Char(' ') + + m_perfRecordProcess->arguments().join(QLatin1Char(' ')); } else { return {}; } @@ -329,11 +296,13 @@ const QString PerfRecord::perfCommand() void PerfRecord::stopRecording() { m_userTerminated = true; - if (m_elevatePrivilegesProcess) { - m_elevatePrivilegesProcess->terminate(); - } if (m_perfRecordProcess) { - m_perfRecordProcess->terminate(); + if (m_perfControlFifo.isOpen()) { + m_perfControlFifo.requestStop(); + m_targetProcessForPrivilegedPerf.terminate(); + } else { + m_perfRecordProcess->terminate(); + } } } @@ -346,8 +315,7 @@ void PerfRecord::sendInput(const QByteArray& input) QString PerfRecord::sudoUtil() { const auto commands = { - QStringLiteral("pkexec"), QStringLiteral("kdesudo"), QStringLiteral("kdesu"), - // gksudo / gksu seem to close stdin and thus the elevate script doesn't wait on read + QStringLiteral("pkexec"), }; for (const auto& cmd : commands) { QString util = QStandardPaths::findExecutable(cmd); @@ -456,3 +424,8 @@ bool PerfRecord::isPerfInstalled() { return !perfBinaryPath().isEmpty(); } + +bool PerfRecord::actuallyElevatePrivileges(bool elevatePrivileges) +{ + return elevatePrivileges && canElevatePrivileges() && geteuid() != 0 && !privsAlreadyElevated(); +} diff --git a/src/perfrecord.h b/src/perfrecord.h index 933b0d96..0c22b010 100644 --- a/src/perfrecord.h +++ b/src/perfrecord.h @@ -8,6 +8,9 @@ #pragma once +#include "initiallystoppedprocess.h" +#include "perfcontrolfifowrapper.h" + #include #include @@ -55,12 +58,13 @@ class PerfRecord : public QObject private: QPointer m_perfRecordProcess; - QPointer m_elevatePrivilegesProcess; + InitiallyStoppedProcess m_targetProcessForPrivilegedPerf; + PerfControlFifoWrapper m_perfControlFifo; QString m_outputPath; bool m_userTerminated; - void startRecording(bool elevatePrivileges, const QStringList& perfOptions, const QString& outputPath, - const QStringList& recordOptions, const QString& workingDirectory = QString()); - void startRecording(const QStringList& perfOptions, const QString& outputPath, const QStringList& recordOptions, - const QString& workingDirectory = QString()); + static bool actuallyElevatePrivileges(bool elevatePrivileges); + + bool runPerf(bool elevatePrivileges, const QStringList& perfOptions, const QString& outputPath, + const QString& workingDirectory = QString()); }; diff --git a/src/recordpage.cpp b/src/recordpage.cpp index 82169211..b2610579 100644 --- a/src/recordpage.cpp +++ b/src/recordpage.cpp @@ -374,8 +374,11 @@ RecordPage::RecordPage(QWidget* parent) } else if (!PerfRecord::canElevatePrivileges()) { ui->elevatePrivilegesCheckBox->setChecked(false); ui->elevatePrivilegesCheckBox->setEnabled(false); - ui->elevatePrivilegesCheckBox->setText( - tr("(Note: Install pkexec, kdesudo or kdesu to temporarily elevate perf privileges.)")); +#if ALLOW_PRIVILEGE_ESCALATION + ui->elevatePrivilegesCheckBox->setText(tr("(Note: this requires pkexec installed")); +#else + ui->elevatePrivilegesCheckBox->setText(tr("(Note: hotspot is not build with ALLOW_PRIVILEGE_ESCALATION=ON)")); +#endif } connect(ui->elevatePrivilegesCheckBox, &QCheckBox::toggled, this, &RecordPage::updateOffCpuCheckboxState); diff --git a/tests/integrationtests/CMakeLists.txt b/tests/integrationtests/CMakeLists.txt index a7e66717..fedfa8da 100644 --- a/tests/integrationtests/CMakeLists.txt +++ b/tests/integrationtests/CMakeLists.txt @@ -3,6 +3,8 @@ include_directories(../../src/models) include_directories(../../src/parsers/perf) ecm_add_test( + ../../src/initiallystoppedprocess.cpp + ../../src/perfcontrolfifowrapper.cpp ../../src/perfrecord.cpp ../../src/settings.cpp ../../src/util.cpp diff --git a/tests/modeltests/CMakeLists.txt b/tests/modeltests/CMakeLists.txt index 3e967fd0..3ef9fa2a 100644 --- a/tests/modeltests/CMakeLists.txt +++ b/tests/modeltests/CMakeLists.txt @@ -53,6 +53,8 @@ if(KGraphViewerPart_FOUND) ecm_add_test( tst_callgraphgenerator.cpp ../../src/parsers/perf/perfparser.cpp + ../../src/initiallystoppedprocess.cpp + ../../src/perfcontrolfifowrapper.cpp ../../src/perfrecord.cpp ../../src/callgraphgenerator.cpp LINK_LIBRARIES