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 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 committed May 4, 2023
1 parent b2ee046 commit bdb1ebd
Show file tree
Hide file tree
Showing 11 changed files with 353 additions and 199 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.

1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ set(HOTSPOT_SRCS
perfoutputwidgettext.cpp
perfoutputwidgetkonsole.cpp
costcontextmenu.cpp
elevateprivilegeshelper.cpp
# ui files:
mainwindow.ui
aboutdialog.ui
Expand Down
186 changes: 186 additions & 0 deletions src/elevateprivilegeshelper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#include "elevateprivilegeshelper.h"

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

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

InitiallyStoppedProcess::~InitiallyStoppedProcess()
{
kill();
}

bool InitiallyStoppedProcess::reset(const QString& exePath, const QStringList& exeOptions,
const QString& workingDirectory)
{
kill();

// convert arguments and working dir into what the C API needs

std::vector<QByteArray> args;
args.reserve(exeOptions.size() + 1);
args.emplace_back(exePath.toLocal8Bit());
for (const auto& opt : exeOptions)
args.emplace_back(opt.toLocal8Bit());
const auto wd = workingDirectory.toLocal8Bit();

std::vector<char*> argsArray(args.size() + 1);
for (size_t i = 0; i < args.size(); ++i)
argsArray[i] = args[i].data();
argsArray.back() = nullptr;

// fork
m_pid = fork();

if (m_pid == 0) { // inside child process
// change working dir
if (!wd.isEmpty() && chdir(wd.data()) != 0)
qFatal("Failed to change working directory to %s", wd.data());

// stop self
raise(SIGSTOP);

// exec
execvp(argsArray[0], argsArray.data());
qFatal("Failed to exec %s", argsArray[0]);
} else if (m_pid < 0) {
qCritical("Failed to fork (?)");
return false;
}

return true;
}

bool InitiallyStoppedProcess::run()
{
if (m_pid <= 0)
return false;

// wait for child to be stopped

int wstatus;
waitpid(m_pid, &wstatus, WUNTRACED);
if (!WIFSTOPPED(wstatus)) {
m_pid = -1;
return false;
}

// continue

::kill(m_pid, SIGCONT);
return true;
}

void InitiallyStoppedProcess::terminate()
{
if (m_pid > 0)
::kill(m_pid, SIGTERM);
}

void InitiallyStoppedProcess::kill()
{
if (m_pid > 0) {
::kill(m_pid, SIGKILL);
waitpid(m_pid, nullptr, 0);
m_pid = -1;
}
}

PerfControlFifoWrapper::~PerfControlFifoWrapper()
{
close();
}

bool PerfControlFifoWrapper::open()
{
close();

QString fifoParentPath = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
if (fifoParentPath.isEmpty())
fifoParentPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation);

const auto fifoBasePath =
QStringLiteral("%1/hotspot-%2-%3-perf")
.arg(fifoParentPath, QString::number(getpid()), QUuid::createUuid().toString().mid(1, 6));
m_ctlFifoPath = fifoBasePath + QStringLiteral("-control.fifo");
m_ackFifoPath = fifoBasePath + QStringLiteral("-ack.fifo");

if (mkfifo(m_ctlFifoPath.toLocal8Bit().data(), 0600) != 0) {
qCritical() << "Cannot create fifo" << m_ctlFifoPath;
return false;
}
if (mkfifo(m_ackFifoPath.toLocal8Bit().data(), 0600) != 0) {
qCritical() << "Cannot create fifo" << m_ackFifoPath;
return false;
}

m_ctlFifoFd = ::open(m_ctlFifoPath.toLocal8Bit().data(), O_RDWR);
if (m_ctlFifoFd < 0) {
qCritical() << "Cannot open fifo" << m_ctlFifoPath;
return false;
}
m_ackFifoFd = ::open(m_ackFifoPath.toLocal8Bit().data(), O_RDONLY | O_NONBLOCK);
if (m_ackFifoFd < 0) {
qCritical() << "Cannot open fifo" << m_ackFifoPath;
return false;
}

return true;
}

void PerfControlFifoWrapper::start()
{
if (m_ctlFifoFd < 0)
return;

m_ackReady = new QSocketNotifier(m_ackFifoFd, QSocketNotifier::Read);
connect(m_ackReady, &QSocketNotifier::activated, this, [this]() {
char buf[10];
read(m_ackFifoFd, buf, sizeof(buf));
emit started();
m_ackReady->disconnect(this);
});

const char start_cmd[] = "enable\n";
write(m_ctlFifoFd, start_cmd, sizeof(start_cmd) - 1);
}

void PerfControlFifoWrapper::stop()
{
if (m_ctlFifoFd < 0)
return;
const char stop_cmd[] = "stop\n";
write(m_ctlFifoFd, stop_cmd, sizeof(stop_cmd) - 1);
}

void PerfControlFifoWrapper::close()
{
if (m_ackReady) {
delete 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();
}
}
76 changes: 76 additions & 0 deletions src/elevateprivilegeshelper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
SPDX-FileCopyrightText: Zeno Endemann <[email protected]>
SPDX-FileCopyrightText: 2016-2023 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected]
SPDX-License-Identifier: GPL-2.0-or-later
*/

#pragma once

#include <QObject>
#include <QString>
#include <QStringList>

#include <unistd.h>

class QSocketNotifier;

class InitiallyStoppedProcess
{
public:
InitiallyStoppedProcess() = default;
~InitiallyStoppedProcess();

InitiallyStoppedProcess(const InitiallyStoppedProcess&) = delete;
InitiallyStoppedProcess operator=(const InitiallyStoppedProcess&) = delete;

pid_t processPID() const
{
return m_pid;
}

bool reset(const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory);
bool run();
void terminate();
void kill();

private:
pid_t m_pid = -1;
};

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 start();
void stop();
void close();

signals:
void started();

private:
QSocketNotifier* m_ackReady = nullptr;
QString m_ctlFifoPath;
QString m_ackFifoPath;
int m_ctlFifoFd = -1;
int m_ackFifoFd = -1;
};
Loading

0 comments on commit bdb1ebd

Please sign in to comment.