Skip to content

Commit

Permalink
Implement new perf elevated privilege system
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
zeno-endemann-kdab authored and milianw committed Jul 21, 2023
1 parent 5a6280b commit ab73817
Show file tree
Hide file tree
Showing 14 changed files with 483 additions and 203 deletions.
1 change: 0 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 0 additions & 8 deletions scripts/CMakeLists.txt

This file was deleted.

65 changes: 0 additions & 65 deletions scripts/elevate_perf_privileges.sh

This file was deleted.

2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ set(HOTSPOT_SRCS
perfoutputwidgettext.cpp
perfoutputwidgetkonsole.cpp
costcontextmenu.cpp
initiallystoppedprocess.cpp
perfcontrolfifowrapper.cpp
# ui files:
mainwindow.ui
aboutdialog.ui
Expand Down
131 changes: 131 additions & 0 deletions src/initiallystoppedprocess.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
SPDX-FileCopyrightText: Zeno Endemann <zeno.endemann@kdab.com>
SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
SPDX-License-Identifier: GPL-2.0-or-later
*/

#include <cstring>

#include "initiallystoppedprocess.h"

#include <QDebug>
#include <QFile>
#include <QLoggingCategory>
#include <QSocketNotifier>
#include <QStandardPaths>

#include <fcntl.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <vector>

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<QByteArray> argsQBA;
std::vector<char*> 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;
}
}
47 changes: 47 additions & 0 deletions src/initiallystoppedprocess.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
SPDX-FileCopyrightText: Zeno Endemann <zeno.endemann@kdab.com>
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 <QString>
#include <QStringList>
#include <QtGlobal>

#include <unistd.h>

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;
};
Loading

0 comments on commit ab73817

Please sign in to comment.