Merge pull request #4910 from khaneliman/hyprland

fix(hyprland): misc hardening with ipc socket and events
This commit is contained in:
Alexis Rouillard
2026-03-08 22:21:40 +01:00
committed by GitHub
8 changed files with 190 additions and 114 deletions

View File

@@ -20,8 +20,8 @@ class Window : public waybar::AAppIconLabel, public EventHandler {
private: private:
struct Workspace { struct Workspace {
int id; int id = 0;
int windows; int windows = 0;
std::string last_window; std::string last_window;
std::string last_window_title; std::string last_window_title;
@@ -29,14 +29,14 @@ class Window : public waybar::AAppIconLabel, public EventHandler {
}; };
struct WindowData { struct WindowData {
bool floating; bool floating = false;
int monitor = -1; int monitor = -1;
std::string class_name; std::string class_name;
std::string initial_class_name; std::string initial_class_name;
std::string title; std::string title;
std::string initial_title; std::string initial_title;
bool fullscreen; bool fullscreen = false;
bool grouped; bool grouped = false;
static auto parse(const Json::Value&) -> WindowData; static auto parse(const Json::Value&) -> WindowData;
}; };
@@ -47,7 +47,7 @@ class Window : public waybar::AAppIconLabel, public EventHandler {
void queryActiveWorkspace(); void queryActiveWorkspace();
void setClass(const std::string&, bool enable); void setClass(const std::string&, bool enable);
bool separateOutputs_; bool separateOutputs_ = false;
std::mutex mutex_; std::mutex mutex_;
const Bar& bar_; const Bar& bar_;
util::JsonParser parser_; util::JsonParser parser_;
@@ -55,11 +55,11 @@ class Window : public waybar::AAppIconLabel, public EventHandler {
Workspace workspace_; Workspace workspace_;
std::string soloClass_; std::string soloClass_;
std::string lastSoloClass_; std::string lastSoloClass_;
bool solo_; bool solo_ = false;
bool allFloating_; bool allFloating_ = false;
bool swallowing_; bool swallowing_ = false;
bool fullscreen_; bool fullscreen_ = false;
bool focused_; bool focused_ = false;
IPC& m_ipc; IPC& m_ipc;
}; };

View File

@@ -4,6 +4,7 @@
#include <gtkmm/enums.h> #include <gtkmm/enums.h>
#include <gtkmm/label.h> #include <gtkmm/label.h>
#include <json/value.h> #include <json/value.h>
#include <sigc++/connection.h>
#include <cstdint> #include <cstdint>
#include <map> #include <map>
@@ -208,6 +209,7 @@ class Workspaces : public AModule, public EventHandler {
std::mutex m_mutex; std::mutex m_mutex;
const Bar& m_bar; const Bar& m_bar;
Gtk::Box m_box; Gtk::Box m_box;
sigc::connection m_scrollEventConnection_;
IPC& m_ipc; IPC& m_ipc;
}; };

View File

@@ -25,11 +25,7 @@ std::filesystem::path IPC::getSocketFolder(const char* instanceSig) {
static std::mutex folderMutex; static std::mutex folderMutex;
std::unique_lock lock(folderMutex); std::unique_lock lock(folderMutex);
// socket path, specified by EventManager of Hyprland if (socketFolder_.empty()) {
if (!socketFolder_.empty()) {
return socketFolder_;
}
const char* xdgRuntimeDirEnv = std::getenv("XDG_RUNTIME_DIR"); const char* xdgRuntimeDirEnv = std::getenv("XDG_RUNTIME_DIR");
std::filesystem::path xdgRuntimeDir; std::filesystem::path xdgRuntimeDir;
// Only set path if env variable is set // Only set path if env variable is set
@@ -43,9 +39,9 @@ std::filesystem::path IPC::getSocketFolder(const char* instanceSig) {
spdlog::warn("$XDG_RUNTIME_DIR/hypr does not exist, falling back to /tmp/hypr"); spdlog::warn("$XDG_RUNTIME_DIR/hypr does not exist, falling back to /tmp/hypr");
socketFolder_ = std::filesystem::path("/tmp") / "hypr"; socketFolder_ = std::filesystem::path("/tmp") / "hypr";
} }
}
socketFolder_ = socketFolder_ / instanceSig; return socketFolder_ / instanceSig;
return socketFolder_;
} }
IPC::IPC() { IPC::IPC() {
@@ -91,7 +87,7 @@ void IPC::socketListener() {
spdlog::info("Hyprland IPC starting"); spdlog::info("Hyprland IPC starting");
struct sockaddr_un addr; struct sockaddr_un addr = {};
const int socketfd = socket(AF_UNIX, SOCK_STREAM, 0); const int socketfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (socketfd == -1) { if (socketfd == -1) {
@@ -102,10 +98,13 @@ void IPC::socketListener() {
addr.sun_family = AF_UNIX; addr.sun_family = AF_UNIX;
auto socketPath = IPC::getSocketFolder(his) / ".socket2.sock"; auto socketPath = IPC::getSocketFolder(his) / ".socket2.sock";
if (socketPath.native().size() >= sizeof(addr.sun_path)) {
spdlog::error("Hyprland IPC: Socket path is too long: {}", socketPath.string());
close(socketfd);
return;
}
strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1);
addr.sun_path[sizeof(addr.sun_path) - 1] = 0;
int l = sizeof(struct sockaddr_un); int l = sizeof(struct sockaddr_un);
if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) { if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) {
@@ -233,8 +232,10 @@ std::string IPC::getSocket1Reply(const std::string& rq) {
std::string socketPath = IPC::getSocketFolder(instanceSig) / ".socket.sock"; std::string socketPath = IPC::getSocketFolder(instanceSig) / ".socket.sock";
// Use snprintf to copy the socketPath string into serverAddress.sun_path // Use snprintf to copy the socketPath string into serverAddress.sun_path
if (snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str()) < const auto socketPathLength =
0) { snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str());
if (socketPathLength < 0 ||
socketPathLength >= static_cast<int>(sizeof(serverAddress.sun_path))) {
throw std::runtime_error("Hyprland IPC: Couldn't copy socket path (6)"); throw std::runtime_error("Hyprland IPC: Couldn't copy socket path (6)");
} }
@@ -243,15 +244,28 @@ std::string IPC::getSocket1Reply(const std::string& rq) {
throw std::runtime_error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)"); throw std::runtime_error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)");
} }
auto sizeWritten = write(serverSocket, rq.c_str(), rq.length()); std::size_t totalWritten = 0;
while (totalWritten < rq.length()) {
const auto sizeWritten =
write(serverSocket, rq.c_str() + totalWritten, rq.length() - totalWritten);
if (sizeWritten < 0) { if (sizeWritten < 0) {
if (errno == EINTR) {
continue;
}
spdlog::error("Hyprland IPC: Couldn't write (4)"); spdlog::error("Hyprland IPC: Couldn't write (4)");
return ""; return "";
} }
if (sizeWritten == 0) {
spdlog::error("Hyprland IPC: Socket write made no progress");
return "";
}
totalWritten += static_cast<std::size_t>(sizeWritten);
}
std::array<char, 8192> buffer = {0}; std::array<char, 8192> buffer = {0};
std::string response; std::string response;
ssize_t sizeWritten = 0;
do { do {
sizeWritten = read(serverSocket, buffer.data(), 8192); sizeWritten = read(serverSocket, buffer.data(), 8192);

View File

@@ -63,19 +63,35 @@ auto Language::update() -> void {
void Language::onEvent(const std::string& ev) { void Language::onEvent(const std::string& ev) {
std::lock_guard<std::mutex> lg(mutex_); std::lock_guard<std::mutex> lg(mutex_);
std::string kbName(begin(ev) + ev.find_last_of('>') + 1, begin(ev) + ev.find_first_of(',')); const auto payloadStart = ev.find(">>");
if (payloadStart == std::string::npos) {
spdlog::warn("hyprland language received malformed event: {}", ev);
return;
}
const auto payload = ev.substr(payloadStart + 2);
const auto kbSeparator = payload.find(',');
if (kbSeparator == std::string::npos) {
spdlog::warn("hyprland language received malformed event payload: {}", ev);
return;
}
std::string kbName = payload.substr(0, kbSeparator);
// Last comma before variants parenthesis, eg: // Last comma before variants parenthesis, eg:
// activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl., // activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl.,
// with dead keys) // with dead keys)
std::string beforeParenthesis; std::string beforeParenthesis;
auto parenthesisPos = ev.find_last_of('('); auto parenthesisPos = payload.find_last_of('(');
if (parenthesisPos == std::string::npos) { if (parenthesisPos == std::string::npos) {
beforeParenthesis = ev; beforeParenthesis = payload;
} else { } else {
beforeParenthesis = std::string(begin(ev), begin(ev) + parenthesisPos); beforeParenthesis = payload.substr(0, parenthesisPos);
} }
auto layoutName = ev.substr(beforeParenthesis.find_last_of(',') + 1); const auto layoutSeparator = beforeParenthesis.find_last_of(',');
if (layoutSeparator == std::string::npos) {
spdlog::warn("hyprland language received malformed layout payload: {}", ev);
return;
}
auto layoutName = payload.substr(layoutSeparator + 1);
if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString())
return; // ignore return; // ignore

View File

@@ -75,7 +75,12 @@ void Submap::onEvent(const std::string& ev) {
return; return;
} }
auto submapName = ev.substr(ev.find_first_of('>') + 2); const auto separator = ev.find(">>");
if (separator == std::string::npos) {
spdlog::warn("hyprland submap received malformed event: {}", ev);
return;
}
auto submapName = ev.substr(separator + 2);
submap_ = submapName; submap_ = submapName;

View File

@@ -32,7 +32,6 @@ Window::Window(const std::string& id, const Bar& bar, const Json::Value& config)
windowIpcUniqueLock.unlock(); windowIpcUniqueLock.unlock();
queryActiveWorkspace();
update(); update();
dp.emit(); dp.emit();
} }
@@ -177,27 +176,38 @@ auto Window::WindowData::parse(const Json::Value& value) -> Window::WindowData {
} }
void Window::queryActiveWorkspace() { void Window::queryActiveWorkspace() {
std::shared_lock<std::shared_mutex> windowIpcShareLock(windowIpcSmtx);
if (separateOutputs_) { if (separateOutputs_) {
workspace_ = getActiveWorkspace(this->bar_.output->name); workspace_ = getActiveWorkspace(this->bar_.output->name);
} else { } else {
workspace_ = getActiveWorkspace(); workspace_ = getActiveWorkspace();
} }
focused_ = true; focused_ = false;
if (workspace_.windows > 0) { windowData_ = WindowData{};
allFloating_ = false;
swallowing_ = false;
fullscreen_ = false;
solo_ = false;
soloClass_.clear();
if (workspace_.windows <= 0) {
return;
}
const auto clients = m_ipc.getSocket1JsonReply("clients"); const auto clients = m_ipc.getSocket1JsonReply("clients");
if (clients.isArray()) { if (!clients.isArray()) {
return;
}
auto activeWindow = std::ranges::find_if(clients, [&](const Json::Value& window) { auto activeWindow = std::ranges::find_if(clients, [&](const Json::Value& window) {
return window["address"] == workspace_.last_window; return window["address"] == workspace_.last_window;
}); });
if (activeWindow == std::end(clients)) { if (activeWindow == std::end(clients)) {
focused_ = false;
return; return;
} }
focused_ = true;
windowData_ = WindowData::parse(*activeWindow); windowData_ = WindowData::parse(*activeWindow);
updateAppIconName(windowData_.class_name, windowData_.initial_class_name); updateAppIconName(windowData_.class_name, windowData_.initial_class_name);
std::vector<Json::Value> workspaceWindows; std::vector<Json::Value> workspaceWindows;
@@ -225,18 +235,6 @@ void Window::queryActiveWorkspace() {
if (solo_) { if (solo_) {
soloClass_ = windowData_.class_name; soloClass_ = windowData_.class_name;
} else {
soloClass_ = "";
}
}
} else {
focused_ = false;
windowData_ = WindowData{};
allFloating_ = false;
swallowing_ = false;
fullscreen_ = false;
solo_ = false;
soloClass_ = "";
} }
} }

View File

@@ -34,6 +34,9 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value&
} }
Workspaces::~Workspaces() { Workspaces::~Workspaces() {
if (m_scrollEventConnection_.connected()) {
m_scrollEventConnection_.disconnect();
}
m_ipc.unregisterForIPC(this); m_ipc.unregisterForIPC(this);
// wait for possible event handler to finish // wait for possible event handler to finish
std::lock_guard<std::mutex> lg(m_mutex); std::lock_guard<std::mutex> lg(m_mutex);
@@ -44,9 +47,13 @@ void Workspaces::init() {
initializeWorkspaces(); initializeWorkspaces();
if (m_scrollEventConnection_.connected()) {
m_scrollEventConnection_.disconnect();
}
if (barScroll()) { if (barScroll()) {
auto& window = const_cast<Bar&>(m_bar).window; auto& window = const_cast<Bar&>(m_bar).window;
window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
m_scrollEventConnection_ =
window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll)); window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll));
} }
@@ -271,7 +278,7 @@ void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const& clientsJs
// key is the workspace and value is array of monitors to create on // key is the workspace and value is array of monitors to create on
for (const Json::Value& monitor : value) { for (const Json::Value& monitor : value) {
if (monitor.isString() && monitor.asString() == currentMonitor) { if (monitor.isString() && monitor.asString() == currentMonitor) {
persistentWorkspacesToCreate.emplace_back(currentMonitor); persistentWorkspacesToCreate.emplace_back(key);
break; break;
} }
} }
@@ -332,8 +339,13 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value& c
void Workspaces::onEvent(const std::string& ev) { void Workspaces::onEvent(const std::string& ev) {
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); const auto separator = ev.find(">>");
std::string payload = ev.substr(eventName.size() + 2); if (separator == std::string::npos) {
spdlog::warn("Malformed Hyprland workspace event: {}", ev);
return;
}
std::string eventName = ev.substr(0, separator);
std::string payload = ev.substr(separator + 2);
if (eventName == "workspacev2") { if (eventName == "workspacev2") {
onWorkspaceActivated(payload); onWorkspaceActivated(payload);
@@ -496,19 +508,21 @@ void Workspaces::onMonitorFocused(std::string const& payload) {
void Workspaces::onWindowOpened(std::string const& payload) { void Workspaces::onWindowOpened(std::string const& payload) {
spdlog::trace("Window opened: {}", payload); spdlog::trace("Window opened: {}", payload);
updateWindowCount(); updateWindowCount();
size_t lastCommaIdx = 0; const auto firstComma = payload.find(',');
size_t nextCommaIdx = payload.find(','); const auto secondComma =
std::string windowAddress = payload.substr(lastCommaIdx, nextCommaIdx - lastCommaIdx); firstComma == std::string::npos ? std::string::npos : payload.find(',', firstComma + 1);
const auto thirdComma =
secondComma == std::string::npos ? std::string::npos : payload.find(',', secondComma + 1);
if (firstComma == std::string::npos || secondComma == std::string::npos ||
thirdComma == std::string::npos) {
spdlog::warn("Malformed Hyprland openwindow payload: {}", payload);
return;
}
lastCommaIdx = nextCommaIdx; std::string windowAddress = payload.substr(0, firstComma);
nextCommaIdx = payload.find(',', nextCommaIdx + 1); std::string workspaceName = payload.substr(firstComma + 1, secondComma - firstComma - 1);
std::string workspaceName = payload.substr(lastCommaIdx + 1, nextCommaIdx - lastCommaIdx - 1); std::string windowClass = payload.substr(secondComma + 1, thirdComma - secondComma - 1);
std::string windowTitle = payload.substr(thirdComma + 1);
lastCommaIdx = nextCommaIdx;
nextCommaIdx = payload.find(',', nextCommaIdx + 1);
std::string windowClass = payload.substr(lastCommaIdx + 1, nextCommaIdx - lastCommaIdx - 1);
std::string windowTitle = payload.substr(nextCommaIdx + 1, payload.length() - nextCommaIdx);
bool isActive = m_currentActiveWindowAddress == windowAddress; bool isActive = m_currentActiveWindowAddress == windowAddress;
m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowClass, windowTitle, isActive); m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowClass, windowTitle, isActive);
@@ -1002,10 +1016,12 @@ void Workspaces::sortWorkspaces() {
void Workspaces::setUrgentWorkspace(std::string const& windowaddress) { void Workspaces::setUrgentWorkspace(std::string const& windowaddress) {
const Json::Value clientsJson = m_ipc.getSocket1JsonReply("clients"); const Json::Value clientsJson = m_ipc.getSocket1JsonReply("clients");
const std::string normalizedAddress =
windowaddress.starts_with("0x") ? windowaddress : fmt::format("0x{}", windowaddress);
int workspaceId = -1; int workspaceId = -1;
for (const auto& clientJson : clientsJson) { for (const auto& clientJson : clientsJson) {
if (clientJson["address"].asString().ends_with(windowaddress)) { if (clientJson["address"].asString() == normalizedAddress) {
workspaceId = clientJson["workspace"]["id"].asInt(); workspaceId = clientJson["workspace"]["id"].asInt();
break; break;
} }
@@ -1134,7 +1150,11 @@ std::string Workspaces::makePayload(Args const&... args) {
} }
std::pair<std::string, std::string> Workspaces::splitDoublePayload(std::string const& payload) { std::pair<std::string, std::string> Workspaces::splitDoublePayload(std::string const& payload) {
const std::string part1 = payload.substr(0, payload.find(',')); const auto separator = payload.find(',');
if (separator == std::string::npos) {
throw std::invalid_argument("Expected a two-part Hyprland payload");
}
const std::string part1 = payload.substr(0, separator);
const std::string part2 = payload.substr(part1.size() + 1); const std::string part2 = payload.substr(part1.size() + 1);
return {part1, part2}; return {part1, part2};
} }
@@ -1143,6 +1163,9 @@ std::tuple<std::string, std::string, std::string> Workspaces::splitTriplePayload
std::string const& payload) { std::string const& payload) {
const size_t firstComma = payload.find(','); const size_t firstComma = payload.find(',');
const size_t secondComma = payload.find(',', firstComma + 1); const size_t secondComma = payload.find(',', firstComma + 1);
if (firstComma == std::string::npos || secondComma == std::string::npos) {
throw std::invalid_argument("Expected a three-part Hyprland payload");
}
const std::string part1 = payload.substr(0, firstComma); const std::string part1 = payload.substr(0, firstComma);
const std::string part2 = payload.substr(firstComma + 1, secondComma - (firstComma + 1)); const std::string part2 = payload.substr(firstComma + 1, secondComma - (firstComma + 1));

View File

@@ -86,6 +86,24 @@ TEST_CASE("XDGRuntimeDirExistsNoHyprDir", "[getSocketFolder]") {
fs::remove_all(tempDir, ec); fs::remove_all(tempDir, ec);
} }
TEST_CASE("Socket folder is resolved per instance signature", "[getSocketFolder]") {
const fs::path tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000";
std::error_code ec;
fs::remove_all(tempDir, ec);
fs::create_directories(tempDir / "hypr");
setenv("XDG_RUNTIME_DIR", tempDir.c_str(), 1);
IPCTestHelper::resetSocketFolder();
const auto firstPath = hyprland::IPC::getSocketFolder("instance_a");
const auto secondPath = hyprland::IPC::getSocketFolder("instance_b");
REQUIRE(firstPath == tempDir / "hypr" / "instance_a");
REQUIRE(secondPath == tempDir / "hypr" / "instance_b");
REQUIRE(firstPath != secondPath);
fs::remove_all(tempDir, ec);
}
TEST_CASE("getSocket1Reply throws on no socket", "[getSocket1Reply]") { TEST_CASE("getSocket1Reply throws on no socket", "[getSocket1Reply]") {
unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); unsetenv("HYPRLAND_INSTANCE_SIGNATURE");
IPCTestHelper::resetSocketFolder(); IPCTestHelper::resetSocketFolder();