From bb1ff10497c856b516a4ff6a72905db63e0ccf16 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Tue, 30 Dec 2025 21:48:11 +0000 Subject: [PATCH 1/4] feat: Implement Landlock sandboxing for launched applications and update audio device detection to use `pw-dump`. --- CMakeLists.txt | 1 + shell/src/audioroutingmanager.cpp | 215 ++++++++---------------- shell/src/audioroutingmanager.h | 1 - shell/src/wayland/waylandcompositor.cpp | 19 ++- tools/marathon-sandbox/CMakeLists.txt | 9 + tools/marathon-sandbox/main.c | 174 +++++++++++++++++++ 6 files changed, 270 insertions(+), 149 deletions(-) create mode 100644 tools/marathon-sandbox/CMakeLists.txt create mode 100644 tools/marathon-sandbox/main.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5016f455a..e0f646421 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,7 @@ add_subdirectory(marathon-ui) add_subdirectory(shell) add_subdirectory(tools/marathon-dev) add_subdirectory(tools/marathon-app-runner) +add_subdirectory(tools/marathon-sandbox) message(STATUS "=== Marathon OS ===") message(STATUS "Qt version: ${Qt6_VERSION}") diff --git a/shell/src/audioroutingmanager.cpp b/shell/src/audioroutingmanager.cpp index 5bf7effd1..b33b8070b 100644 --- a/shell/src/audioroutingmanager.cpp +++ b/shell/src/audioroutingmanager.cpp @@ -1,5 +1,7 @@ #include "audioroutingmanager.h" -#include +#include +#include +#include AudioRoutingManager::AudioRoutingManager(QObject *parent) : QObject(parent) @@ -199,125 +201,76 @@ void AudioRoutingManager::onWpctlFinished(int exitCode, QProcess::ExitStatus exi } void AudioRoutingManager::detectAudioDevices() { - // Run wpctl status to discover audio devices - QProcess wpctl; - wpctl.start("wpctl", QStringList() << "status"); - - if (!wpctl.waitForFinished(5000)) { - qWarning() << "[AudioRoutingManager] wpctl command timed out or failed"; - QString error = wpctl.readAllStandardError(); - qWarning() << "[AudioRoutingManager] wpctl error:" << error; - m_deviceDetectionTimer->stop(); + QProcess pwDump; + pwDump.start("pw-dump", QStringList()); + + if (!pwDump.waitForFinished(5000)) { + qWarning() << "[AudioRoutingManager] pw-dump command timed out or failed"; return; } - QString output = wpctl.readAllStandardOutput(); - QString error = wpctl.readAllStandardError(); + QByteArray output = pwDump.readAllStandardOutput(); + if (output.isEmpty()) { + qWarning() << "[AudioRoutingManager] pw-dump returned empty output!"; + return; + } - if (!error.isEmpty()) { - qWarning() << "[AudioRoutingManager] wpctl stderr:" << error; + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(output, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "[AudioRoutingManager] Failed to parse pw-dump JSON:" + << parseError.errorString(); + return; } - if (output.isEmpty()) { - qWarning() << "[AudioRoutingManager] wpctl returned empty output!"; + + if (!doc.isArray()) { + qWarning() << "[AudioRoutingManager] pw-dump output is not a JSON array"; return; } - parseWpctlStatus(output); -} -void AudioRoutingManager::parseWpctlStatus(const QString &output) { - // Parse wpctl status output to find: - // - Audio card ID - // - Earpiece sink ID - // - Speaker sink ID - // - Bluetooth sink ID (if available) - // - Microphone source ID - - // Example output format: - // Audio - // ├─ Devices: - // │ 53. Built-in Audio [alsa] - // │ - // ├─ Sinks: - // │ 54. Built-in Audio Analog Stereo [vol: 0.65] - // │ - // ├─ Sources: - // │ 55. Built-in Audio Analog Stereo [vol: 0.80] - - QStringList lines = output.split('\n'); - bool inDevices = false; - bool inSinks = false; - bool inSources = false; - - for (const QString &line : lines) { - // Section detection - if (line.contains("Devices:")) { - inDevices = true; - inSinks = false; - inSources = false; - continue; - } else if (line.contains("Sinks:")) { - inDevices = false; - inSinks = true; - inSources = false; - continue; - } else if (line.contains("Sources:")) { - inDevices = false; - inSinks = false; - inSources = true; - continue; + QJsonArray objects = doc.array(); + for (const QJsonValue &val : objects) { + QJsonObject obj = val.toObject(); + QString type = obj["type"].toString(); + int id = obj["id"].toInt(); + QJsonObject info = obj["info"].toObject(); + QJsonObject props = info["props"].toObject(); + + if (type.contains("PipeWire:Interface:Device")) { + QString deviceName = props["device.name"].toString(); + if (deviceName.contains("alsa_card") && m_audioCardId.isEmpty()) { + m_audioCardId = QString::number(id); + qInfo() << "[AudioRoutingManager] Found audio card:" << id << deviceName; + } } - // Extract device/sink/source IDs - // Match lines like: " │ 42. Virtio 1.0 sound [alsa]" - // or: " * 51. Virtio 1.0 sound Stereo [vol: 0.47]" - // Note: wpctl uses box-drawing characters, so match any non-digit chars before the number - QRegularExpression idRegex(R"((\d+)\.\s+(.+?)(?:\s+\[|$))"); - QRegularExpressionMatch match = idRegex.match(line); - - if (match.hasMatch()) { - QString id = match.captured(1); - QString name = match.captured(2).trimmed(); - - if (inDevices) { - if (name.contains("Built-in", Qt::CaseInsensitive) || - name.contains("Audio", Qt::CaseInsensitive) || - name.contains("sound", Qt::CaseInsensitive) || - name.contains("Sound", Qt::CaseInsensitive)) { - // Use first matching audio device as card ID - if (m_audioCardId.isEmpty()) { - m_audioCardId = id; - qInfo() << "[AudioRoutingManager] ✓ Found audio card:" << id << name; - } - } - } else if (inSinks) { - // Heuristic: detect earpiece vs speaker by name - if (name.contains("Earpiece", Qt::CaseInsensitive)) { - if (m_earpieceSinkId.isEmpty()) { - m_earpieceSinkId = id; - qDebug() << "[AudioRoutingManager] Found earpiece:" << id << name; - } - } else if (name.contains("Speaker", Qt::CaseInsensitive) || - name.contains("Stereo", Qt::CaseInsensitive)) { - if (m_speakerSinkId.isEmpty()) { - m_speakerSinkId = id; - qDebug() << "[AudioRoutingManager] Found speaker:" << id << name; - } - } else if (name.contains("Bluetooth", Qt::CaseInsensitive) || - name.contains("bluez", Qt::CaseInsensitive)) { - if (m_bluetoothSinkId.isEmpty()) { - m_bluetoothSinkId = id; - qDebug() << "[AudioRoutingManager] Found Bluetooth:" << id << name; - } - } - - // Fallback: use first sink as default - if (m_earpieceSinkId.isEmpty() && m_speakerSinkId.isEmpty()) { - m_earpieceSinkId = id; // Assume first sink is earpiece + if (type.contains("PipeWire:Interface:Node")) { + QString mediaClass = props["media.class"].toString(); + QString nodeName = props["node.name"].toString(); + + if (mediaClass == "Audio/Sink") { + if (nodeName.contains("earpiece", Qt::CaseInsensitive) && + m_earpieceSinkId.isEmpty()) { + m_earpieceSinkId = QString::number(id); + qDebug() << "[AudioRoutingManager] Found earpiece:" << id << nodeName; + } else if ((nodeName.contains("speaker", Qt::CaseInsensitive) || + nodeName.contains("stereo", Qt::CaseInsensitive)) && + m_speakerSinkId.isEmpty()) { + m_speakerSinkId = QString::number(id); + qDebug() << "[AudioRoutingManager] Found speaker:" << id << nodeName; + } else if ((nodeName.contains("bluetooth", Qt::CaseInsensitive) || + nodeName.contains("bluez", Qt::CaseInsensitive)) && + m_bluetoothSinkId.isEmpty()) { + m_bluetoothSinkId = QString::number(id); + qDebug() << "[AudioRoutingManager] Found Bluetooth:" << id << nodeName; + } else if (m_speakerSinkId.isEmpty()) { + m_speakerSinkId = QString::number(id); } - } else if (inSources) { - if (m_microphoneSourceId.isEmpty()) { - m_microphoneSourceId = id; - qDebug() << "[AudioRoutingManager] Found microphone:" << id << name; + } else if (mediaClass == "Audio/Source") { + if (!nodeName.contains("monitor", Qt::CaseInsensitive) && + m_microphoneSourceId.isEmpty()) { + m_microphoneSourceId = QString::number(id); + qDebug() << "[AudioRoutingManager] Found microphone:" << id << nodeName; } } } @@ -329,43 +282,15 @@ void AudioRoutingManager::parseWpctlStatus(const QString &output) { } QString AudioRoutingManager::findAudioCard() { - QProcess wpctl; - wpctl.start("wpctl", QStringList() << "status"); - - if (!wpctl.waitForFinished(5000)) { - return QString(); - } - - QString output = wpctl.readAllStandardOutput(); - - // Find first audio device - QRegularExpression regex(R"((\d+)\.\s+.*Built-in.*Audio)"); - QRegularExpressionMatch match = regex.match(output); - - if (match.hasMatch()) { - return match.captured(1); - } - - return QString(); + return m_audioCardId; } QString AudioRoutingManager::findSinkByName(const QString &name) { - QProcess wpctl; - wpctl.start("wpctl", QStringList() << "status"); - - if (!wpctl.waitForFinished(5000)) { - return QString(); - } - - QString output = wpctl.readAllStandardOutput(); - - // Find sink matching name - QRegularExpression regex(QString(R"((\d+)\.\s+.*%1)").arg(QRegularExpression::escape(name))); - QRegularExpressionMatch match = regex.match(output); - - if (match.hasMatch()) { - return match.captured(1); - } - - return QString(); + if (name.contains("earpiece", Qt::CaseInsensitive)) + return m_earpieceSinkId; + if (name.contains("speaker", Qt::CaseInsensitive)) + return m_speakerSinkId; + if (name.contains("bluetooth", Qt::CaseInsensitive)) + return m_bluetoothSinkId; + return m_speakerSinkId; } diff --git a/shell/src/audioroutingmanager.h b/shell/src/audioroutingmanager.h index d017d97be..8844a6ae7 100644 --- a/shell/src/audioroutingmanager.h +++ b/shell/src/audioroutingmanager.h @@ -92,7 +92,6 @@ class AudioRoutingManager : public QObject { void runWpctlCommand(const QString &command, const QStringList &args); QString findAudioCard(); QString findSinkByName(const QString &name); - void parseWpctlStatus(const QString &output); bool m_isInCall; bool m_isSpeakerphoneEnabled; diff --git a/shell/src/wayland/waylandcompositor.cpp b/shell/src/wayland/waylandcompositor.cpp index 2ec9d9df4..1b1847c66 100644 --- a/shell/src/wayland/waylandcompositor.cpp +++ b/shell/src/wayland/waylandcompositor.cpp @@ -388,12 +388,25 @@ void WaylandCompositor::launchApp(const QString &command) { } }); + const bool useSandbox = !isFlatpak && !isSnap && envBool("MARATHON_ENABLE_SANDBOX", true); + if (!needsShell) { - process->setProgram(program); - process->setArguments(args); + if (useSandbox) { + QStringList sandboxArgs; + sandboxArgs << program << args; + process->setProgram("marathon-sandbox"); + process->setArguments(sandboxArgs); + } else { + process->setProgram(program); + process->setArguments(args); + } process->start(); } else { - process->start("/bin/sh", {"-c", actualCommand}); + if (useSandbox) { + process->start("marathon-sandbox", {"/bin/sh", "-c", actualCommand}); + } else { + process->start("/bin/sh", {"-c", actualCommand}); + } } } diff --git a/tools/marathon-sandbox/CMakeLists.txt b/tools/marathon-sandbox/CMakeLists.txt new file mode 100644 index 000000000..50a2bc8e2 --- /dev/null +++ b/tools/marathon-sandbox/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) +project(marathon-sandbox LANGUAGES C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +add_executable(marathon-sandbox main.c) + +install(TARGETS marathon-sandbox RUNTIME DESTINATION bin) diff --git a/tools/marathon-sandbox/main.c b/tools/marathon-sandbox/main.c new file mode 100644 index 000000000..53e4a698e --- /dev/null +++ b/tools/marathon-sandbox/main.c @@ -0,0 +1,174 @@ +/* + * marathon-sandbox - Lightweight Landlock-based sandboxing for Marathon apps + * + * Usage: marathon-sandbox [args...] + * + * Applies filesystem restrictions using Landlock, then execs the command. + * This provides security without the overhead of bubblewrap/systemd-run and + * without breaking Wayland compositor surface sharing. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef landlock_create_ruleset +static inline int landlock_create_ruleset( + const struct landlock_ruleset_attr *attr, size_t size, __u32 flags) +{ + return syscall(__NR_landlock_create_ruleset, attr, size, flags); +} +#endif + +#ifndef landlock_add_rule +static inline int landlock_add_rule(int ruleset_fd, enum landlock_rule_type rule_type, + const void *rule_attr, __u32 flags) +{ + return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags); +} +#endif + +#ifndef landlock_restrict_self +static inline int landlock_restrict_self(int ruleset_fd, __u32 flags) +{ + return syscall(__NR_landlock_restrict_self, ruleset_fd, flags); +} +#endif + +#define ACCESS_FS_ROUGHLY_READ ( \ + LANDLOCK_ACCESS_FS_EXECUTE | \ + LANDLOCK_ACCESS_FS_READ_FILE | \ + LANDLOCK_ACCESS_FS_READ_DIR) + +#define ACCESS_FS_ROUGHLY_WRITE ( \ + LANDLOCK_ACCESS_FS_WRITE_FILE | \ + LANDLOCK_ACCESS_FS_REMOVE_DIR | \ + LANDLOCK_ACCESS_FS_REMOVE_FILE | \ + LANDLOCK_ACCESS_FS_MAKE_CHAR | \ + LANDLOCK_ACCESS_FS_MAKE_DIR | \ + LANDLOCK_ACCESS_FS_MAKE_REG | \ + LANDLOCK_ACCESS_FS_MAKE_SOCK | \ + LANDLOCK_ACCESS_FS_MAKE_FIFO | \ + LANDLOCK_ACCESS_FS_MAKE_BLOCK | \ + LANDLOCK_ACCESS_FS_MAKE_SYM | \ + LANDLOCK_ACCESS_FS_TRUNCATE) + +static int add_path_rule(int ruleset_fd, const char *path, __u64 allowed_access) +{ + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = allowed_access, + }; + + path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC); + if (path_beneath.parent_fd < 0) { + return -1; + } + + int ret = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0); + close(path_beneath.parent_fd); + return ret; +} + +static int setup_landlock(void) +{ + int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); + if (abi < 0) { + if (errno == ENOSYS || errno == EOPNOTSUPP) { + fprintf(stderr, "[marathon-sandbox] Landlock not available, running without sandbox\n"); + return 0; + } + return -1; + } + + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE, + }; + + if (abi < 2) { + ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER; + } + if (abi < 3) { + ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE; + } + + int ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) { + perror("[marathon-sandbox] Failed to create ruleset"); + return -1; + } + + /* Read-only access to system directories */ + add_path_rule(ruleset_fd, "/usr", ACCESS_FS_ROUGHLY_READ); + add_path_rule(ruleset_fd, "/lib", ACCESS_FS_ROUGHLY_READ); + add_path_rule(ruleset_fd, "/lib64", ACCESS_FS_ROUGHLY_READ); + add_path_rule(ruleset_fd, "/bin", ACCESS_FS_ROUGHLY_READ); + add_path_rule(ruleset_fd, "/sbin", ACCESS_FS_ROUGHLY_READ); + add_path_rule(ruleset_fd, "/etc", ACCESS_FS_ROUGHLY_READ); + add_path_rule(ruleset_fd, "/opt", ACCESS_FS_ROUGHLY_READ); + add_path_rule(ruleset_fd, "/proc", ACCESS_FS_ROUGHLY_READ); + add_path_rule(ruleset_fd, "/sys", ACCESS_FS_ROUGHLY_READ); + + /* Device access - needed for GPU/DRI, audio, input */ + add_path_rule(ruleset_fd, "/dev", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + add_path_rule(ruleset_fd, "/dev/dri", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + add_path_rule(ruleset_fd, "/dev/shm", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + + /* Runtime directories - Wayland, D-Bus, PipeWire, etc. */ + add_path_rule(ruleset_fd, "/run", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + add_path_rule(ruleset_fd, "/var", ACCESS_FS_ROUGHLY_READ); + + /* Read-write access to user directories */ + const char *home = getenv("HOME"); + if (home) { + add_path_rule(ruleset_fd, home, ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + } + + /* XDG runtime dir (Wayland socket, etc.) */ + const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); + if (xdg_runtime) { + add_path_rule(ruleset_fd, xdg_runtime, ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + } + + /* Temp directories */ + add_path_rule(ruleset_fd, "/tmp", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + add_path_rule(ruleset_fd, "/var/tmp", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + + /* Apply Landlock */ + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + perror("[marathon-sandbox] Failed to set no_new_privs"); + close(ruleset_fd); + return -1; + } + + if (landlock_restrict_self(ruleset_fd, 0)) { + perror("[marathon-sandbox] Failed to enforce ruleset"); + close(ruleset_fd); + return -1; + } + + close(ruleset_fd); + return 0; +} + +int main(int argc, char *argv[]) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s [args...]\n", argv[0]); + return 1; + } + + if (setup_landlock() < 0) { + fprintf(stderr, "[marathon-sandbox] Warning: continuing without sandbox\n"); + } + + execvp(argv[1], &argv[1]); + perror("[marathon-sandbox] exec failed"); + return 127; +} From 4b46e2ef964c5f10cc94f98b5458258112dda0f2 Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Tue, 30 Dec 2025 23:33:39 +0000 Subject: [PATCH 2/4] feat: add security logging and IPC rate limiting, and enhance app installation and verification with path validation. --- marathon-core/src/marathonappinstaller.cpp | 49 ++++-- marathon-core/src/marathonappverifier.cpp | 10 ++ shell/src/ipc/shellservices.cpp | 35 ++++- shell/src/ratelimiter.h | 87 ++++++++++ shell/src/securitylogger.h | 114 ++++++++++++++ shell/src/securitymanager.cpp | 87 +++++++--- shell/src/securitymanager.h | 44 +++--- tools/marathon-sandbox/main.c | 175 ++++++++++++++++++--- 8 files changed, 525 insertions(+), 76 deletions(-) create mode 100644 shell/src/ratelimiter.h create mode 100644 shell/src/securitylogger.h diff --git a/marathon-core/src/marathonappinstaller.cpp b/marathon-core/src/marathonappinstaller.cpp index 45f256dd4..23c46be25 100644 --- a/marathon-core/src/marathonappinstaller.cpp +++ b/marathon-core/src/marathonappinstaller.cpp @@ -9,6 +9,7 @@ #include #include #include +#include MarathonAppInstaller::MarathonAppInstaller(MarathonAppRegistry *registry, MarathonAppScanner *scanner, QObject *parent) @@ -141,21 +142,52 @@ bool MarathonAppInstaller::installFromDirectory(const QString &sourcePath) { QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); file.close(); - QString appId = doc.object().value("id").toString(); - QString appName = doc.object().value("name").toString(); + QString appId = doc.object().value("id").toString(); + QString appName = doc.object().value("name").toString(); + QString entryPoint = doc.object().value("entryPoint").toString(); - emit installStarted(appId); + if (appId.isEmpty() || appId.contains("..") || appId.contains('/') || appId.contains('\\') || + appId.startsWith('.')) { + qWarning() << "[MarathonAppInstaller] SECURITY: Invalid appId detected:" << appId; + syslog(LOG_AUTH | LOG_ALERT, "[SECURITY] Path traversal blocked: appId=%s", + appId.toUtf8().constData()); + emit installFailed("unknown", "Invalid app ID"); + return false; + } + + if (!entryPoint.isEmpty()) { + if (entryPoint.contains("..") || entryPoint.startsWith('/') || + entryPoint.startsWith('\\')) { + qWarning() << "[MarathonAppInstaller] SECURITY: Invalid entryPoint detected:" + << entryPoint; + syslog(LOG_AUTH | LOG_ALERT, "[SECURITY] Path traversal blocked: app=%s entryPoint=%s", + appId.toUtf8().constData(), entryPoint.toUtf8().constData()); + emit installFailed(appId, "Invalid entry point"); + return false; + } + } + + emit installStarted(appId); - // Check if already installed if (m_registry->hasApp(appId)) { qDebug() << "[MarathonAppInstaller] App already installed, updating:" << appId; } - // Determine destination - QString installBase = getTargetInstallPath(); - QString destPath = installBase + "/" + appId; + QString installBase = getTargetInstallPath(); + QString destPath = installBase + "/" + appId; + + QFileInfo destInfo(destPath); + QString canonicalDest = destInfo.absoluteFilePath(); + QFileInfo baseInfo(installBase); + QString canonicalBase = baseInfo.canonicalFilePath(); + + if (!canonicalBase.isEmpty() && !canonicalDest.startsWith(canonicalBase + "/")) { + qWarning() << "[MarathonAppInstaller] SECURITY: Path escape attempt detected:" + << "dest=" << canonicalDest << "base=" << canonicalBase; + emit installFailed(appId, "Security violation: installation path escape"); + return false; + } - // Remove existing installation if (QDir(destPath).exists()) { if (!removeDirectory(destPath)) { emit installFailed(appId, "Failed to remove existing installation"); @@ -165,7 +197,6 @@ bool MarathonAppInstaller::installFromDirectory(const QString &sourcePath) { emit installProgress(appId, 50); - // Copy directory if (!copyDirectory(sourcePath, destPath)) { emit installFailed(appId, "Failed to copy app files"); return false; diff --git a/marathon-core/src/marathonappverifier.cpp b/marathon-core/src/marathonappverifier.cpp index ee5c5c61e..9eb66f43d 100644 --- a/marathon-core/src/marathonappverifier.cpp +++ b/marathon-core/src/marathonappverifier.cpp @@ -6,6 +6,7 @@ #include #include #include +#include MarathonAppVerifier::MarathonAppVerifier(QObject *parent) : QObject(parent) { @@ -91,6 +92,8 @@ bool MarathonAppVerifier::verifySignatureFile(const QString &manifestPath, // Check for "Good signature" if (output.contains("Good signature", Qt::CaseInsensitive)) { qDebug() << "[MarathonAppVerifier] Signature verification successful"; + syslog(LOG_AUTH | LOG_INFO, "[SECURITY] App signature verified: manifest=%s", + manifestPath.toUtf8().constData()); return true; } @@ -98,6 +101,8 @@ bool MarathonAppVerifier::verifySignatureFile(const QString &manifestPath, if (output.contains("BAD signature", Qt::CaseInsensitive)) { m_lastError = "Invalid GPG signature detected"; qWarning() << "[MarathonAppVerifier] BAD signature detected"; + syslog(LOG_AUTH | LOG_ALERT, "[SECURITY] Invalid app signature: manifest=%s", + manifestPath.toUtf8().constData()); return false; } @@ -105,11 +110,16 @@ bool MarathonAppVerifier::verifySignatureFile(const QString &manifestPath, if (keyFiles.isEmpty() && gpgVerify.exitCode() == 0) { qDebug() << "[MarathonAppVerifier] No trusted keys configured, accepting valid signature " "(DEV MODE)"; + syslog(LOG_AUTH | LOG_WARNING, + "[SECURITY] App accepted without trusted key (dev mode): manifest=%s", + manifestPath.toUtf8().constData()); return true; } m_lastError = "GPG verification failed: " + output; qWarning() << "[MarathonAppVerifier] Verification failed:" << output; + syslog(LOG_AUTH | LOG_WARNING, "[SECURITY] App signature verification failed: manifest=%s", + manifestPath.toUtf8().constData()); return false; } diff --git a/shell/src/ipc/shellservices.cpp b/shell/src/ipc/shellservices.cpp index 77434a4b7..05e92b117 100644 --- a/shell/src/ipc/shellservices.cpp +++ b/shell/src/ipc/shellservices.cpp @@ -9,6 +9,8 @@ #include "../powermanagercpp.h" #include "../audiomanagercpp.h" #include "../audiopolicycontroller.h" +#include "../ratelimiter.h" +#include "../securitylogger.h" #include "callhistorymanager.h" #include "contactsmanager.h" @@ -23,8 +25,12 @@ #include #include -static bool hasAnyPermission(MarathonPermissionManager *pm, const QString &appId, - const QStringList &perms) { +static RateLimiter g_ipcRateLimiter; +static constexpr int RATE_LIMIT_MAX_CALLS = 30; +static constexpr int RATE_LIMIT_WINDOW_MS = 1000; + +static bool hasAnyPermission(MarathonPermissionManager *pm, const QString &appId, + const QStringList &perms) { if (!pm) return false; for (const auto &p : perms) { @@ -34,6 +40,19 @@ static bool hasAnyPermission(MarathonPermissionManager *pm, const QString &appId return false; } +static bool checkRateLimit(const QDBusContext &ctx, const QString &method) { + if (!ctx.calledFromDBus()) + return true; + + const QString sender = ctx.message().service(); + if (!g_ipcRateLimiter.tryAcquire(sender, method, RATE_LIMIT_MAX_CALLS, RATE_LIMIT_WINDOW_MS)) { + double rate = g_ipcRateLimiter.getRate(sender, method); + SecurityLogger::logRateLimitExceeded(sender, method, static_cast(rate)); + return false; + } + return true; +} + static QString dbusCallerAppIdOrEmpty(const QDBusContext &ctx, AppLaunchService *als) { if (!ctx.calledFromDBus() || !als) return {}; @@ -171,12 +190,18 @@ QString SettingsObject::callerAppIdOrEmpty() const { } bool SettingsObject::requireSystem() { + if (!checkRateLimit(*this, "Settings")) { + sendErrorReply(QDBusError::LimitsExceeded, "Rate limit exceeded"); + return false; + } + const QString caller = callerAppIdOrEmpty(); if (caller.isEmpty()) { sendErrorReply(QDBusError::AccessDenied, "Unknown caller"); return false; } if (!hasAnyPermission(m_permissions, caller, {"system"})) { + SecurityLogger::logPermissionDenied(caller, "system"); sendErrorReply(QDBusError::AccessDenied, "Missing permission: system"); return false; } @@ -184,12 +209,18 @@ bool SettingsObject::requireSystem() { } bool SettingsObject::requireStorage() { + if (!checkRateLimit(*this, "Settings.Storage")) { + sendErrorReply(QDBusError::LimitsExceeded, "Rate limit exceeded"); + return false; + } + const QString caller = callerAppIdOrEmpty(); if (caller.isEmpty()) { sendErrorReply(QDBusError::AccessDenied, "Unknown caller"); return false; } if (!hasAnyPermission(m_permissions, caller, {"storage"})) { + SecurityLogger::logPermissionDenied(caller, "storage"); sendErrorReply(QDBusError::AccessDenied, "Missing permission: storage"); return false; } diff --git a/shell/src/ratelimiter.h b/shell/src/ratelimiter.h new file mode 100644 index 000000000..c09adecbf --- /dev/null +++ b/shell/src/ratelimiter.h @@ -0,0 +1,87 @@ +#ifndef RATELIMITER_H +#define RATELIMITER_H + +#include +#include +#include +#include +#include + +class RateLimiter { + public: + bool tryAcquire(const QString &callerId, const QString &method, int maxCalls = 10, + int windowMs = 1000) { + QMutexLocker locker(&m_mutex); + + QString key = callerId + "::" + method; + qint64 now = QDateTime::currentMSecsSinceEpoch(); + + CallRecord &record = m_records[key]; + + while (!record.timestamps.isEmpty() && (now - record.timestamps.first()) > windowMs) { + record.timestamps.removeFirst(); + } + + if (record.timestamps.size() >= maxCalls) { + record.blocked = true; + return false; + } + + record.timestamps.append(now); + record.blocked = false; + return true; + } + + double getRate(const QString &callerId, const QString &method) const { + QMutexLocker locker(&m_mutex); + + QString key = callerId + "::" + method; + if (!m_records.contains(key)) { + return 0.0; + } + + const CallRecord &record = m_records[key]; + if (record.timestamps.size() < 2) { + return 0.0; + } + + qint64 now = QDateTime::currentMSecsSinceEpoch(); + qint64 oldest = record.timestamps.first(); + qint64 span = now - oldest; + + if (span <= 0) { + return 0.0; + } + + return (double)record.timestamps.size() / (span / 1000.0); + } + + void clear() { + QMutexLocker locker(&m_mutex); + m_records.clear(); + } + + void clearCaller(const QString &callerId) { + QMutexLocker locker(&m_mutex); + QStringList toRemove; + for (auto it = m_records.begin(); it != m_records.end(); ++it) { + if (it.key().startsWith(callerId + "::")) { + toRemove.append(it.key()); + } + } + for (const QString &key : toRemove) { + m_records.remove(key); + } + } + + private: + struct CallRecord { + QList timestamps; + bool blocked = false; + }; + + mutable QMutex m_mutex; + QHash m_records; +}; + +#endif diff --git a/shell/src/securitylogger.h b/shell/src/securitylogger.h new file mode 100644 index 000000000..9e6b294a5 --- /dev/null +++ b/shell/src/securitylogger.h @@ -0,0 +1,114 @@ +#ifndef SECURITYLOGGER_H +#define SECURITYLOGGER_H + +#include +#include +#include +#include + +class SecurityLogger { + public: + enum EventType { + AuthSuccess, + AuthFailure, + AuthLockout, + PermissionGranted, + PermissionDenied, + PermissionRequested, + SandboxViolation, + RateLimitExceeded, + PathTraversalBlocked, + InvalidInput + }; + + static void initialize(const QString &appName = "marathon-shell") { + static bool initialized = false; + if (!initialized) { + openlog(appName.toUtf8().constData(), LOG_PID | LOG_NDELAY, LOG_AUTH); + initialized = true; + } + } + + static void logAuthSuccess(const QString &username, const QString &method) { + log(AuthSuccess, + QString("Authentication success: user=%1 method=%2").arg(username, method)); + } + + static void logAuthFailure(const QString &username, const QString &reason, + const QString &source = QString()) { + QString msg = QString("Authentication failure: user=%1 reason=%2").arg(username, reason); + if (!source.isEmpty()) { + msg += QString(" source=%1").arg(source); + } + log(AuthFailure, msg); + } + + static void logAuthLockout(const QString &username, int durationSeconds) { + log(AuthLockout, + QString("Account locked out: user=%1 duration=%2s").arg(username).arg(durationSeconds)); + } + + static void logPermissionGranted(const QString &appId, const QString &permission) { + log(PermissionGranted, + QString("Permission granted: app=%1 permission=%2").arg(appId, permission)); + } + + static void logPermissionDenied(const QString &appId, const QString &permission) { + log(PermissionDenied, + QString("Permission denied: app=%1 permission=%2").arg(appId, permission)); + } + + static void logPermissionRequested(const QString &appId, const QString &permission) { + log(PermissionRequested, + QString("Permission requested: app=%1 permission=%2").arg(appId, permission)); + } + + static void logSandboxViolation(const QString &appId, const QString &attemptedPath) { + log(SandboxViolation, + QString("Sandbox violation: app=%1 path=%2").arg(appId, attemptedPath)); + } + + static void logRateLimitExceeded(const QString &callerId, const QString &method, + int callsPerSecond) { + log(RateLimitExceeded, + QString("Rate limit exceeded: caller=%1 method=%2 rate=%3/s") + .arg(callerId, method) + .arg(callsPerSecond)); + } + + static void logPathTraversalBlocked(const QString &source, const QString &maliciousPath) { + log(PathTraversalBlocked, + QString("Path traversal blocked: source=%1 path=%2").arg(source, maliciousPath)); + } + + static void logInvalidInput(const QString &source, const QString &field, + const QString &reason) { + log(InvalidInput, + QString("Invalid input rejected: source=%1 field=%2 reason=%3") + .arg(source, field, reason)); + } + + private: + static void log(EventType type, const QString &message) { + QString fullMessage = QString("[SECURITY] %1").arg(message); + qWarning().noquote() << fullMessage; + + int priority = LOG_AUTH; + switch (type) { + case AuthSuccess: + case PermissionGranted: priority |= LOG_INFO; break; + case AuthFailure: + case PermissionDenied: + case PermissionRequested: priority |= LOG_WARNING; break; + case AuthLockout: + case SandboxViolation: + case RateLimitExceeded: + case PathTraversalBlocked: + case InvalidInput: priority |= LOG_ALERT; break; + } + + syslog(priority, "%s", fullMessage.toUtf8().constData()); + } +}; + +#endif diff --git a/shell/src/securitymanager.cpp b/shell/src/securitymanager.cpp index d9db7fa0e..d945c656d 100644 --- a/shell/src/securitymanager.cpp +++ b/shell/src/securitymanager.cpp @@ -1,4 +1,5 @@ #include "securitymanager.h" +#include "securitylogger.h" #include #include #include @@ -26,6 +27,9 @@ SecurityManager::SecurityManager(QObject *parent) , m_fprintdAuthInProgress(false) , m_passwordAuthWatcher(new QFutureWatcher(this)) , m_quickPINAuthWatcher(new QFutureWatcher(this)) { + // Initialize security event logging + SecurityLogger::initialize(); + qDebug() << "[SecurityManager] Initializing authentication system"; // Initialize fprintd @@ -198,21 +202,25 @@ void SecurityManager::authenticatePassword(const QString &password) { } void SecurityManager::onPasswordAuthFinished() { - bool success = m_passwordAuthWatcher->result(); + bool success = m_passwordAuthWatcher->result(); + QString username = getCurrentUsername(); m_currentPassword.clear(); qDebug() << "[SecurityManager] Async PAM authentication completed, success:" << success; if (success) { resetFailedAttempts(); + SecurityLogger::logAuthSuccess(username, "password"); emit authenticationSuccess(); } else { recordFailedAttempt(); + SecurityLogger::logAuthFailure(username, "Invalid password", "PAM"); QString message = "Incorrect password"; if (m_isLockedOut) { int remaining = lockoutSecondsRemaining(); message = QString("Too many failed attempts. Locked for %1 seconds.").arg(remaining); + SecurityLogger::logAuthLockout(username, remaining); } else if (m_failedAttempts > 0) { int remaining = 5 - m_failedAttempts; message += QString(" (%1 attempts remaining)").arg(remaining); @@ -223,11 +231,27 @@ void SecurityManager::onPasswordAuthFinished() { } // ============================================================================ -// Quick PIN (Optional Convenience Feature) +// Quick PIN // ============================================================================ +QByteArray SecurityManager::generateSalt(int length) { + QByteArray salt; + salt.resize(length); + for (int i = 0; i < length; ++i) { + salt[i] = static_cast(QRandomGenerator::securelySeeded().bounded(256)); + } + return salt; +} + +QByteArray SecurityManager::hashWithIterations(const QByteArray &data, int iterations) { + QByteArray result = data; + for (int i = 0; i < iterations; ++i) { + result = QCryptographicHash::hash(result, QCryptographicHash::Sha256); + } + return result; +} + QString SecurityManager::retrieveQuickPIN() { - // For now, store in a simple config file (in production, use libsecret) QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/marathon/quickpin.conf"; QFile file(configPath); @@ -241,18 +265,24 @@ QString SecurityManager::retrieveQuickPIN() { return QString(); } - QString hashedPIN = QString::fromUtf8(file.readAll()); + QString contents = QString::fromUtf8(file.readAll()).trimmed(); file.close(); - return hashedPIN.trimmed(); + return contents; } bool SecurityManager::storeQuickPIN(const QString &pin) { - // Hash the PIN with SHA-256 - QByteArray hash = QCryptographicHash::hash(pin.toUtf8(), QCryptographicHash::Sha256); - QString hashedPIN = QString::fromLatin1(hash.toHex()); + static constexpr int SALT_LENGTH = 32; + static constexpr int ITERATIONS = 10000; + + QByteArray salt = generateSalt(SALT_LENGTH); + QByteArray saltedPin = salt + pin.toUtf8(); + QByteArray hash = hashWithIterations(saltedPin, ITERATIONS); + + QString storedValue = + QString::fromLatin1(salt.toHex()) + ":" + QString::fromLatin1(hash.toHex()); - QString configPath = + QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/marathon"; QDir().mkpath(configPath); @@ -264,10 +294,8 @@ bool SecurityManager::storeQuickPIN(const QString &pin) { return false; } - file.write(hashedPIN.toUtf8()); + file.write(storedValue.toUtf8()); file.close(); - - // Set restrictive permissions (owner read/write only) file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); qDebug() << "[SecurityManager] Quick PIN stored successfully"; @@ -281,15 +309,34 @@ void SecurityManager::clearQuickPIN() { } bool SecurityManager::verifyQuickPIN(const QString &pin) { - QString storedHash = retrieveQuickPIN(); - if (storedHash.isEmpty()) { + static constexpr int ITERATIONS = 10000; + + QString storedData = retrieveQuickPIN(); + if (storedData.isEmpty()) { return false; } - QByteArray hash = QCryptographicHash::hash(pin.toUtf8(), QCryptographicHash::Sha256); - QString hashedPIN = QString::fromLatin1(hash.toHex()); + QStringList parts = storedData.split(':'); + if (parts.size() != 2) { + qWarning() << "[SecurityManager] Legacy PIN format detected"; + QByteArray hash = QCryptographicHash::hash(pin.toUtf8(), QCryptographicHash::Sha256); + return QString::fromLatin1(hash.toHex()) == storedData; + } + + QByteArray salt = QByteArray::fromHex(parts[0].toLatin1()); + QByteArray storedHash = QByteArray::fromHex(parts[1].toLatin1()); + QByteArray saltedPin = salt + pin.toUtf8(); + QByteArray computedHash = hashWithIterations(saltedPin, ITERATIONS); - return hashedPIN == storedHash; + if (computedHash.size() != storedHash.size()) { + return false; + } + + volatile int result = 0; + for (int i = 0; i < computedHash.size(); ++i) { + result |= (computedHash[i] ^ storedHash[i]); + } + return result == 0; } void SecurityManager::authenticateQuickPIN(const QString &pin) { @@ -316,20 +363,24 @@ void SecurityManager::authenticateQuickPIN(const QString &pin) { } void SecurityManager::onQuickPINAuthFinished() { - bool success = m_quickPINAuthWatcher->result(); + bool success = m_quickPINAuthWatcher->result(); + QString username = getCurrentUsername(); qDebug() << "[SecurityManager] Async Quick PIN verification completed, success:" << success; if (success) { resetFailedAttempts(); + SecurityLogger::logAuthSuccess(username, "quick_pin"); emit authenticationSuccess(); } else { recordFailedAttempt(); + SecurityLogger::logAuthFailure(username, "Invalid PIN", "QuickPIN"); QString message = "Incorrect PIN"; if (m_isLockedOut) { int remaining = lockoutSecondsRemaining(); message = QString("Too many failed attempts. Locked for %1 seconds.").arg(remaining); + SecurityLogger::logAuthLockout(username, remaining); } else if (m_failedAttempts > 0) { int remaining = 5 - m_failedAttempts; message += QString(" (%1 attempts remaining)").arg(remaining); diff --git a/shell/src/securitymanager.h b/shell/src/securitymanager.h index afe1f5752..a410c6781 100644 --- a/shell/src/securitymanager.h +++ b/shell/src/securitymanager.h @@ -9,6 +9,7 @@ #include #include #include +#include #include class SecurityManager : public QObject { @@ -96,28 +97,27 @@ class SecurityManager : public QObject { void onQuickPINAuthFinished(); private: - // PAM authentication backend - bool authenticateViaPAM(const QString &password); - static int pamConversationCallback(int num_msg, const struct pam_message **msg, - struct pam_response **resp, void *appdata_ptr); - - // Quick PIN (encrypted in libsecret) - bool verifyQuickPIN(const QString &pin); - bool storeQuickPIN(const QString &pin); - QString retrieveQuickPIN(); - void clearQuickPIN(); - - // fprintd D-Bus integration - void initFingerprintDevice(); - void startFingerprintAuth(); - void stopFingerprintAuth(); - void checkFingerprintEnrollment(); - - // Rate limiting and lockout (query pam_faillock) - void updateLockoutStatus(); - void recordFailedAttempt(); - void resetFailedAttempts(); - int queryFaillockAttempts(); + bool authenticateViaPAM(const QString &password); + static int pamConversationCallback(int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr); + + bool verifyQuickPIN(const QString &pin); + bool storeQuickPIN(const QString &pin); + QString retrieveQuickPIN(); + void clearQuickPIN(); + + static QByteArray generateSalt(int length); + static QByteArray hashWithIterations(const QByteArray &data, int iterations); + + void initFingerprintDevice(); + void startFingerprintAuth(); + void stopFingerprintAuth(); + void checkFingerprintEnrollment(); + + void updateLockoutStatus(); + void recordFailedAttempt(); + void resetFailedAttempts(); + int queryFaillockAttempts(); // Members AuthMode m_authMode; diff --git a/tools/marathon-sandbox/main.c b/tools/marathon-sandbox/main.c index 53e4a698e..541448f9c 100644 --- a/tools/marathon-sandbox/main.c +++ b/tools/marathon-sandbox/main.c @@ -1,23 +1,18 @@ -/* - * marathon-sandbox - Lightweight Landlock-based sandboxing for Marathon apps - * - * Usage: marathon-sandbox [args...] - * - * Applies filesystem restrictions using Landlock, then execs the command. - * This provides security without the overhead of bubblewrap/systemd-run and - * without breaking Wayland compositor surface sharing. - */ - #define _GNU_SOURCE #include #include #include +#include +#include +#include +#include #include #include #include #include #include #include +#include #ifndef landlock_create_ruleset static inline int landlock_create_ruleset( @@ -76,12 +71,135 @@ static int add_path_rule(int ruleset_fd, const char *path, __u64 allowed_access) return ret; } +static int setup_seccomp(void) +{ + if (prctl(PR_GET_SECCOMP) == -1 && errno == EINVAL) { + fprintf(stderr, "[marathon-sandbox] Seccomp not available\n"); + return 0; + } + + static const int blocked_syscalls[] = { + __NR_ptrace, + __NR_process_vm_readv, + __NR_process_vm_writev, +#ifdef __NR_mount + __NR_mount, +#endif +#ifdef __NR_umount2 + __NR_umount2, +#endif +#ifdef __NR_pivot_root + __NR_pivot_root, +#endif +#ifdef __NR_reboot + __NR_reboot, +#endif +#ifdef __NR_kexec_load + __NR_kexec_load, +#endif +#ifdef __NR_kexec_file_load + __NR_kexec_file_load, +#endif +#ifdef __NR_init_module + __NR_init_module, +#endif +#ifdef __NR_finit_module + __NR_finit_module, +#endif +#ifdef __NR_delete_module + __NR_delete_module, +#endif +#ifdef __NR_acct + __NR_acct, +#endif +#ifdef __NR_swapon + __NR_swapon, +#endif +#ifdef __NR_swapoff + __NR_swapoff, +#endif +#ifdef __NR_personality + __NR_personality, +#endif +#ifdef __NR_lookup_dcookie + __NR_lookup_dcookie, +#endif +#ifdef __NR_perf_event_open + __NR_perf_event_open, +#endif +#ifdef __NR_add_key + __NR_add_key, +#endif +#ifdef __NR_request_key + __NR_request_key, +#endif +#ifdef __NR_keyctl + __NR_keyctl, +#endif +#ifdef __NR_mbind + __NR_mbind, +#endif +#ifdef __NR_userfaultfd + __NR_userfaultfd, +#endif +#ifdef __NR_bpf + __NR_bpf, +#endif +#ifdef __NR_open_by_handle_at + __NR_open_by_handle_at, +#endif + }; + + const size_t num_blocked = sizeof(blocked_syscalls) / sizeof(blocked_syscalls[0]); + const size_t filter_size = 2 + (2 * num_blocked) + 1; + struct sock_filter *filter = calloc(filter_size, sizeof(struct sock_filter)); + if (!filter) { + perror("[marathon-sandbox] Failed to allocate seccomp filter"); + return -1; + } + + size_t idx = 0; + + filter[idx++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, + offsetof(struct seccomp_data, nr)); + +#if defined(__x86_64__) + filter[idx++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, + offsetof(struct seccomp_data, arch)); + filter[idx-1] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, + offsetof(struct seccomp_data, nr)); +#endif + + for (size_t i = 0; i < num_blocked; i++) { + filter[idx++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, + blocked_syscalls[i], 0, 1); + filter[idx++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, + SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)); + } + + filter[idx++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); + + struct sock_fprog prog = { + .len = (unsigned short)idx, + .filter = filter, + }; + + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) { + perror("[marathon-sandbox] Failed to apply seccomp filter"); + free(filter); + return -1; + } + + free(filter); + return 0; +} + static int setup_landlock(void) { int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); if (abi < 0) { if (errno == ENOSYS || errno == EOPNOTSUPP) { - fprintf(stderr, "[marathon-sandbox] Landlock not available, running without sandbox\n"); + fprintf(stderr, "[marathon-sandbox] Landlock not available\n"); return 0; } return -1; @@ -104,7 +222,6 @@ static int setup_landlock(void) return -1; } - /* Read-only access to system directories */ add_path_rule(ruleset_fd, "/usr", ACCESS_FS_ROUGHLY_READ); add_path_rule(ruleset_fd, "/lib", ACCESS_FS_ROUGHLY_READ); add_path_rule(ruleset_fd, "/lib64", ACCESS_FS_ROUGHLY_READ); @@ -115,32 +232,30 @@ static int setup_landlock(void) add_path_rule(ruleset_fd, "/proc", ACCESS_FS_ROUGHLY_READ); add_path_rule(ruleset_fd, "/sys", ACCESS_FS_ROUGHLY_READ); - /* Device access - needed for GPU/DRI, audio, input */ add_path_rule(ruleset_fd, "/dev", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); add_path_rule(ruleset_fd, "/dev/dri", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); add_path_rule(ruleset_fd, "/dev/shm", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); - /* Runtime directories - Wayland, D-Bus, PipeWire, etc. */ - add_path_rule(ruleset_fd, "/run", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + add_path_rule(ruleset_fd, "/run/dbus", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + add_path_rule(ruleset_fd, "/run/user", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + + const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); + if (xdg_runtime) { + add_path_rule(ruleset_fd, xdg_runtime, ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + } + add_path_rule(ruleset_fd, "/var", ACCESS_FS_ROUGHLY_READ); + add_path_rule(ruleset_fd, "/var/cache", ACCESS_FS_ROUGHLY_READ); + add_path_rule(ruleset_fd, "/var/lib", ACCESS_FS_ROUGHLY_READ); - /* Read-write access to user directories */ const char *home = getenv("HOME"); if (home) { add_path_rule(ruleset_fd, home, ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); } - /* XDG runtime dir (Wayland socket, etc.) */ - const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); - if (xdg_runtime) { - add_path_rule(ruleset_fd, xdg_runtime, ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); - } - - /* Temp directories */ add_path_rule(ruleset_fd, "/tmp", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); add_path_rule(ruleset_fd, "/var/tmp", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); - /* Apply Landlock */ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { perror("[marathon-sandbox] Failed to set no_new_privs"); close(ruleset_fd); @@ -164,10 +279,20 @@ int main(int argc, char *argv[]) return 1; } + openlog("marathon-sandbox", LOG_PID | LOG_NDELAY, LOG_AUTH); + if (setup_landlock() < 0) { - fprintf(stderr, "[marathon-sandbox] Warning: continuing without sandbox\n"); + syslog(LOG_WARNING, "[SECURITY] Landlock setup failed"); + fprintf(stderr, "[marathon-sandbox] Warning: continuing without Landlock\n"); } + if (setup_seccomp() < 0) { + syslog(LOG_WARNING, "[SECURITY] Seccomp setup failed"); + fprintf(stderr, "[marathon-sandbox] Warning: continuing without seccomp\n"); + } + + closelog(); + execvp(argv[1], &argv[1]); perror("[marathon-sandbox] exec failed"); return 127; From 20d38078db2452a525abfc4cc2256770d33351fa Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Wed, 31 Dec 2025 10:35:06 +0000 Subject: [PATCH 3/4] feat: Introduce 'terminal' permission for apps and add symlink escape verification to app packager. --- apps/terminal/manifest.json | 2 +- marathon-core/src/marathonapppackager.cpp | 91 +++++++++-------- marathon-core/src/marathonapppackager.h | 1 + marathon-core/src/marathonappverifier.cpp | 76 ++++++-------- shell/src/marathonpermissionmanager.cpp | 4 +- tools/marathon-sandbox/main.c | 116 ++++++++++------------ 6 files changed, 136 insertions(+), 154 deletions(-) diff --git a/apps/terminal/manifest.json b/apps/terminal/manifest.json index 4458ae1ff..6b1ede805 100644 --- a/apps/terminal/manifest.json +++ b/apps/terminal/manifest.json @@ -5,7 +5,7 @@ "entryPoint": "TerminalApp.qml", "icon": "assets/icon.svg", "author": "Marathon OS", - "permissions": ["system", "storage"], + "permissions": ["system", "storage", "terminal"], "minShellVersion": "1.0.0", "protected": false, "categories": ["System"], diff --git a/marathon-core/src/marathonapppackager.cpp b/marathon-core/src/marathonapppackager.cpp index f51c69146..d1399fdc0 100644 --- a/marathon-core/src/marathonapppackager.cpp +++ b/marathon-core/src/marathonapppackager.cpp @@ -7,6 +7,7 @@ #include #include #include +#include static bool copyDirRecursively(const QString &sourceDir, const QString &destDir) { QDir src(sourceDir); @@ -31,7 +32,6 @@ static bool copyDirRecursively(const QString &sourceDir, const QString &destDir) return false; } } else { - // Overwrite if it exists if (QFile::exists(dstPath)) { QFile::remove(dstPath); } @@ -46,6 +46,42 @@ static bool copyDirRecursively(const QString &sourceDir, const QString &destDir) MarathonAppPackager::MarathonAppPackager(QObject *parent) : QObject(parent) {} +bool MarathonAppPackager::verifyNoSymlinkEscape(const QString &dir, const QString &baseDir) { + QDir directory(dir); + QString canonicalBase = QFileInfo(baseDir).canonicalFilePath(); + + const QFileInfoList entries = + directory.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System); + + for (const QFileInfo &entry : entries) { + if (entry.isSymLink()) { + QString target = entry.symLinkTarget(); + QFileInfo targetInfo(target); + QString canonicalTarget = targetInfo.canonicalFilePath(); + + if (canonicalTarget.isEmpty()) { + canonicalTarget = + QFileInfo(entry.absolutePath() + "/" + target).canonicalFilePath(); + } + + if (!canonicalTarget.startsWith(canonicalBase + "/") && + canonicalTarget != canonicalBase) { + m_lastError = QString("Symlink escape detected: %1 -> %2") + .arg(entry.absoluteFilePath(), target); + qWarning() << "[MarathonAppPackager] SECURITY:" << m_lastError; + syslog(LOG_AUTH | LOG_ALERT, "[SECURITY] Symlink escape in package: %s -> %s", + entry.absoluteFilePath().toUtf8().constData(), target.toUtf8().constData()); + return false; + } + } else if (entry.isDir()) { + if (!verifyNoSymlinkEscape(entry.absoluteFilePath(), baseDir)) { + return false; + } + } + } + return true; +} + bool MarathonAppPackager::validateAppDirectory(const QString &appDir) { QDir dir(appDir); if (!dir.exists()) { @@ -53,14 +89,12 @@ bool MarathonAppPackager::validateAppDirectory(const QString &appDir) { return false; } - // Check for manifest.json QString manifestPath = appDir + "/manifest.json"; if (!QFile::exists(manifestPath)) { m_lastError = "manifest.json not found in app directory"; return false; } - // Validate manifest QFile manifestFile(manifestPath); if (!manifestFile.open(QIODevice::ReadOnly | QIODevice::Text)) { m_lastError = "Failed to open manifest.json"; @@ -81,10 +115,9 @@ bool MarathonAppPackager::validateAppDirectory(const QString &appDir) { return false; } - QJsonObject obj = doc.object(); - - // Check required fields + QJsonObject obj = doc.object(); QStringList requiredFields = {"id", "name", "version", "entryPoint", "icon"}; + for (const QString &field : requiredFields) { if (!obj.contains(field) || obj.value(field).toString().isEmpty()) { m_lastError = "manifest.json missing required field: " + field; @@ -92,7 +125,6 @@ bool MarathonAppPackager::validateAppDirectory(const QString &appDir) { } } - // Check if entryPoint exists QString entryPoint = obj.value("entryPoint").toString(); if (!QFile::exists(appDir + "/" + entryPoint)) { m_lastError = "Entry point file not found: " + entryPoint; @@ -103,10 +135,7 @@ bool MarathonAppPackager::validateAppDirectory(const QString &appDir) { } bool MarathonAppPackager::createZipArchive(const QString &sourceDir, const QString &zipPath) { - // Use zip command for creating archive - QProcess zipProcess; - - // Change to source directory and create zip + QProcess zipProcess; QStringList args; args << "-r" << zipPath << "."; @@ -118,7 +147,7 @@ bool MarathonAppPackager::createZipArchive(const QString &sourceDir, const QStri return false; } - if (!zipProcess.waitForFinished(30000)) { // 30 second timeout + if (!zipProcess.waitForFinished(30000)) { m_lastError = "Zip process timed out"; return false; } @@ -132,16 +161,13 @@ bool MarathonAppPackager::createZipArchive(const QString &sourceDir, const QStri } bool MarathonAppPackager::extractZipArchive(const QString &zipPath, const QString &destDir) { - // Ensure destination exists QDir dir; if (!dir.mkpath(destDir)) { m_lastError = "Failed to create destination directory: " + destDir; return false; } - // Use unzip command for extraction QProcess unzipProcess; - QStringList args; args << "-o" << zipPath << "-d" << destDir; @@ -152,32 +178,27 @@ bool MarathonAppPackager::extractZipArchive(const QString &zipPath, const QStrin return false; } - if (!unzipProcess.waitForFinished(30000)) { // 30 second timeout + if (!unzipProcess.waitForFinished(30000)) { m_lastError = "Unzip process timed out"; return false; } - if (unzipProcess.exitCode() != 0) { - QString errorMsg = QString::fromUtf8(unzipProcess.readAllStandardError()); - // unzip returns 1 for warnings, which is often ok - if (unzipProcess.exitCode() > 1) { - m_lastError = "Unzip process failed: " + errorMsg; - return false; - } + if (unzipProcess.exitCode() > 1) { + m_lastError = + "Unzip process failed: " + QString::fromUtf8(unzipProcess.readAllStandardError()); + return false; } return true; } bool MarathonAppPackager::verifyPackageStructure(const QString &extractedDir) { - // Verify manifest.json exists QString manifestPath = extractedDir + "/manifest.json"; if (!QFile::exists(manifestPath)) { m_lastError = "Extracted package missing manifest.json"; return false; } - // Validate manifest QFile manifestFile(manifestPath); if (!manifestFile.open(QIODevice::ReadOnly | QIODevice::Text)) { m_lastError = "Failed to read manifest.json from package"; @@ -193,6 +214,10 @@ bool MarathonAppPackager::verifyPackageStructure(const QString &extractedDir) { return false; } + if (!verifyNoSymlinkEscape(extractedDir, extractedDir)) { + return false; + } + return true; } @@ -202,7 +227,6 @@ bool MarathonAppPackager::createPackage(const QString &appDir, const QString &ou emit packagingProgress(10); - // Validate app directory if (!validateAppDirectory(appDir)) { qWarning() << "[MarathonAppPackager] Validation failed:" << m_lastError; emit error(m_lastError); @@ -211,7 +235,6 @@ bool MarathonAppPackager::createPackage(const QString &appDir, const QString &ou emit packagingProgress(30); - // Remove output file if it exists if (QFile::exists(outputPath)) { if (!QFile::remove(outputPath)) { m_lastError = "Failed to remove existing output file: " + outputPath; @@ -222,7 +245,6 @@ bool MarathonAppPackager::createPackage(const QString &appDir, const QString &ou emit packagingProgress(50); - // Create zip archive if (!createZipArchive(appDir, outputPath)) { qWarning() << "[MarathonAppPackager] Failed to create zip:" << m_lastError; emit error(m_lastError); @@ -231,7 +253,6 @@ bool MarathonAppPackager::createPackage(const QString &appDir, const QString &ou emit packagingProgress(90); - // Verify the created package if (!QFile::exists(outputPath)) { m_lastError = "Package file was not created: " + outputPath; emit error(m_lastError); @@ -250,16 +271,14 @@ bool MarathonAppPackager::extractPackage(const QString &packagePath, const QStri emit extractionProgress(10); - // Check if package exists if (!QFile::exists(packagePath)) { m_lastError = "Package file does not exist: " + packagePath; emit error(m_lastError); return false; } - emit extractionProgress(30); + emit extractionProgress(30); - // Create temporary directory for extraction QTemporaryDir tempDir; if (!tempDir.isValid()) { m_lastError = "Failed to create temporary directory"; @@ -267,9 +286,8 @@ bool MarathonAppPackager::extractPackage(const QString &packagePath, const QStri return false; } - emit extractionProgress(50); + emit extractionProgress(50); - // Extract to temp directory first QString tempExtractPath = tempDir.path(); if (!extractZipArchive(packagePath, tempExtractPath)) { qWarning() << "[MarathonAppPackager] Extraction failed:" << m_lastError; @@ -279,7 +297,6 @@ bool MarathonAppPackager::extractPackage(const QString &packagePath, const QStri emit extractionProgress(70); - // Verify package structure if (!verifyPackageStructure(tempExtractPath)) { qWarning() << "[MarathonAppPackager] Package structure invalid:" << m_lastError; emit error(m_lastError); @@ -288,7 +305,6 @@ bool MarathonAppPackager::extractPackage(const QString &packagePath, const QStri emit extractionProgress(85); - // Move to final destination QDir destDirObj(destDir); if (destDirObj.exists()) { if (!destDirObj.removeRecursively()) { @@ -298,7 +314,6 @@ bool MarathonAppPackager::extractPackage(const QString &packagePath, const QStri } } - // Ensure parent directory exists (but DO NOT create destDir itself; rename needs it absent) QDir parentDir = QFileInfo(destDir).dir(); if (!parentDir.exists() && !parentDir.mkpath(".")) { m_lastError = "Failed to create destination parent directory"; @@ -306,9 +321,7 @@ bool MarathonAppPackager::extractPackage(const QString &packagePath, const QStri return false; } - // Prefer atomic-ish move (fast) when possible. if (!QDir().rename(tempExtractPath, destDir)) { - // Cross-filesystem or permissions can cause rename to fail. Fall back to recursive copy. if (!copyDirRecursively(tempExtractPath, destDir)) { m_lastError = "Failed to move extracted files to destination"; emit error(m_lastError); diff --git a/marathon-core/src/marathonapppackager.h b/marathon-core/src/marathonapppackager.h index 4bec04fdc..dc2eb1b75 100644 --- a/marathon-core/src/marathonapppackager.h +++ b/marathon-core/src/marathonapppackager.h @@ -30,6 +30,7 @@ class MarathonAppPackager : public QObject { bool createZipArchive(const QString &sourceDir, const QString &zipPath); bool extractZipArchive(const QString &zipPath, const QString &destDir); bool verifyPackageStructure(const QString &extractedDir); + bool verifyNoSymlinkEscape(const QString &dir, const QString &baseDir); QString m_lastError; }; diff --git a/marathon-core/src/marathonappverifier.cpp b/marathon-core/src/marathonappverifier.cpp index 9eb66f43d..5c2a1a8b3 100644 --- a/marathon-core/src/marathonappverifier.cpp +++ b/marathon-core/src/marathonappverifier.cpp @@ -1,11 +1,11 @@ #include "marathonappverifier.h" +#include #include #include #include #include #include #include -#include #include MarathonAppVerifier::MarathonAppVerifier(QObject *parent) @@ -14,13 +14,8 @@ MarathonAppVerifier::MarathonAppVerifier(QObject *parent) } QString MarathonAppVerifier::getTrustedKeysDir() { - // User-specific trusted keys - QString userDir = - QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/marathon/trusted-keys"; - - // Also check system-wide trusted keys in /usr/share/marathon-shell/trusted-keys/ - // but for now, we'll use user directory - return userDir; + return QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + + "/marathon/trusted-keys"; } bool MarathonAppVerifier::initializeTrustedKeysDir() { @@ -57,19 +52,14 @@ bool MarathonAppVerifier::verifySignatureFile(const QString &manifestPath, return false; } - // Import trusted keys to a temporary keyring QString trustedKeysDir = getTrustedKeysDir(); QDir keysDir(trustedKeysDir); QStringList keyFiles = keysDir.entryList(QStringList() << "*.asc" << "*.gpg", QDir::Files); if (keyFiles.isEmpty()) { - qDebug() << "[MarathonAppVerifier] No trusted keys found, accepting any valid signature " - "(DEV MODE)"; - // In development mode, accept any valid signature - // In production, this should return false + qDebug() << "[MarathonAppVerifier] No trusted keys found, accepting any valid signature"; } - // Verify signature QProcess gpgVerify; QStringList args; args << "--verify" << signaturePath << manifestPath; @@ -86,10 +76,8 @@ bool MarathonAppVerifier::verifySignatureFile(const QString &manifestPath, return false; } - // GPG writes verification output to stderr QString output = QString::fromUtf8(gpgVerify.readAllStandardError()); - // Check for "Good signature" if (output.contains("Good signature", Qt::CaseInsensitive)) { qDebug() << "[MarathonAppVerifier] Signature verification successful"; syslog(LOG_AUTH | LOG_INFO, "[SECURITY] App signature verified: manifest=%s", @@ -97,7 +85,6 @@ bool MarathonAppVerifier::verifySignatureFile(const QString &manifestPath, return true; } - // Check for "BAD signature" if (output.contains("BAD signature", Qt::CaseInsensitive)) { m_lastError = "Invalid GPG signature detected"; qWarning() << "[MarathonAppVerifier] BAD signature detected"; @@ -106,10 +93,17 @@ bool MarathonAppVerifier::verifySignatureFile(const QString &manifestPath, return false; } - // If no trusted keys and signature is valid, allow in dev mode + bool strictMode = qEnvironmentVariableIsSet("MARATHON_REQUIRE_SIGNATURES"); + if (keyFiles.isEmpty() && gpgVerify.exitCode() == 0) { - qDebug() << "[MarathonAppVerifier] No trusted keys configured, accepting valid signature " - "(DEV MODE)"; + if (strictMode) { + m_lastError = "No trusted keys configured (strict mode requires trusted keys)"; + qWarning() << "[MarathonAppVerifier] Rejected: no trusted keys in strict mode"; + syslog(LOG_AUTH | LOG_ALERT, "[SECURITY] App rejected - no trusted keys: manifest=%s", + manifestPath.toUtf8().constData()); + return false; + } + qWarning() << "[MarathonAppVerifier] No trusted keys configured, accepting valid signature"; syslog(LOG_AUTH | LOG_WARNING, "[SECURITY] App accepted without trusted key (dev mode): manifest=%s", manifestPath.toUtf8().constData()); @@ -129,7 +123,6 @@ MarathonAppVerifier::verifyDirectory(const QString &appDir) { emit verificationStarted(); - // Check if directory exists QDir dir(appDir); if (!dir.exists()) { m_lastError = "Directory does not exist: " + appDir; @@ -138,7 +131,6 @@ MarathonAppVerifier::verifyDirectory(const QString &appDir) { return VerificationFailed; } - // Check for manifest.json QString manifestPath = appDir + "/manifest.json"; if (!QFile::exists(manifestPath)) { m_lastError = "manifest.json not found in directory"; @@ -147,23 +139,25 @@ MarathonAppVerifier::verifyDirectory(const QString &appDir) { return ManifestMissing; } - // Check for SIGNATURE.txt QString signaturePath = appDir + "/SIGNATURE.txt"; if (!QFile::exists(signaturePath)) { - // In development mode, allow unsigned packages - qWarning() << "[MarathonAppVerifier] SIGNATURE.txt not found (development mode - allowing " - "unsigned)"; + bool strictMode = qEnvironmentVariableIsSet("MARATHON_REQUIRE_SIGNATURES"); + if (strictMode) { + m_lastError = "SIGNATURE.txt not found (strict mode requires signatures)"; + qWarning() << "[MarathonAppVerifier] Rejected: no signature in strict mode"; + syslog(LOG_AUTH | LOG_ALERT, "[SECURITY] Unsigned app rejected (strict mode): dir=%s", + appDir.toUtf8().constData()); + emit error(m_lastError); + emit verificationComplete(SignatureFileMissing); + return SignatureFileMissing; + } + qWarning() << "[MarathonAppVerifier] SIGNATURE.txt not found (allowing in dev mode)"; + syslog(LOG_AUTH | LOG_WARNING, "[SECURITY] Unsigned app allowed (dev mode): dir=%s", + appDir.toUtf8().constData()); emit verificationComplete(Valid); - return Valid; // For now, allow unsigned in development - - // In production, uncomment this: - // m_lastError = "SIGNATURE.txt not found in directory"; - // emit error(m_lastError); - // emit verificationComplete(SignatureFileMissing); - // return SignatureFileMissing; + return Valid; } - // Verify signature if (!verifySignatureFile(manifestPath, signaturePath)) { emit error(m_lastError); emit verificationComplete(InvalidSignature); @@ -181,7 +175,6 @@ MarathonAppVerifier::verifyPackage(const QString &packagePath) { emit verificationStarted(); - // Check if package exists if (!QFile::exists(packagePath)) { m_lastError = "Package file does not exist: " + packagePath; emit error(m_lastError); @@ -189,9 +182,6 @@ MarathonAppVerifier::verifyPackage(const QString &packagePath) { return VerificationFailed; } - // For .marathon packages, we need to extract and verify - // This will be called by the installer after extraction - m_lastError = "Direct package verification not yet implemented. Extract first, then verify directory."; emit verificationComplete(VerificationFailed); @@ -212,7 +202,6 @@ bool MarathonAppVerifier::signManifest(const QString &manifestPath, const QStrin return false; } - // Create detached signature QProcess gpgSign; QStringList args; args << "--detach-sign" << "--armor" << "--output" << signaturePath; @@ -242,7 +231,6 @@ bool MarathonAppVerifier::signManifest(const QString &manifestPath, const QStrin return false; } - // Verify the signature was created if (!QFile::exists(signaturePath)) { m_lastError = "Signature file was not created"; return false; @@ -253,10 +241,8 @@ bool MarathonAppVerifier::signManifest(const QString &manifestPath, const QStrin } bool MarathonAppVerifier::isTrustedKey(const QString &keyFingerprint) { - QString trustedKeysDir = getTrustedKeysDir(); - QDir keysDir(trustedKeysDir); - - // Check if any key file matches this fingerprint + QString trustedKeysDir = getTrustedKeysDir(); + QDir keysDir(trustedKeysDir); QStringList keyFiles = keysDir.entryList(QStringList() << "*.asc" << "*.gpg", QDir::Files); for (const QString &keyFile : keyFiles) { @@ -278,7 +264,6 @@ bool MarathonAppVerifier::addTrustedKey(const QString &keyPath) { QString fileName = QFileInfo(keyPath).fileName(); QString destPath = trustedKeysDir + "/" + fileName; - // Copy key to trusted keys directory if (QFile::exists(destPath)) { if (!QFile::remove(destPath)) { m_lastError = "Failed to remove existing key file"; @@ -298,7 +283,6 @@ bool MarathonAppVerifier::addTrustedKey(const QString &keyPath) { bool MarathonAppVerifier::removeTrustedKey(const QString &keyFingerprint) { QString trustedKeysDir = getTrustedKeysDir(); QDir keysDir(trustedKeysDir); - QStringList keyFiles = keysDir.entryList(QStringList() << "*.asc" << "*.gpg", QDir::Files); for (const QString &keyFile : keyFiles) { diff --git a/shell/src/marathonpermissionmanager.cpp b/shell/src/marathonpermissionmanager.cpp index d454614c5..f48d1400e 100644 --- a/shell/src/marathonpermissionmanager.cpp +++ b/shell/src/marathonpermissionmanager.cpp @@ -11,7 +11,6 @@ MarathonPermissionManager::MarathonPermissionManager(QObject *parent) : QObject(parent) , m_promptActive(false) { - // Initialize permission descriptions m_permissionDescriptions = { {"network", "Access the internet and make network connections"}, {"location", "Access your device's location"}, @@ -24,7 +23,8 @@ MarathonPermissionManager::MarathonPermissionManager(QObject *parent) {"telephony", "Make and receive phone calls"}, {"sms", "Send and receive SMS messages"}, {"bluetooth", "Connect to Bluetooth devices"}, - {"system", "Access system-level features (restricted to system apps)"}}; + {"system", "Access system-level features (restricted to system apps)"}, + {"terminal", "Run shell commands with full system access (DANGEROUS)"}}; loadPermissions(); diff --git a/tools/marathon-sandbox/main.c b/tools/marathon-sandbox/main.c index 541448f9c..11b7a98bf 100644 --- a/tools/marathon-sandbox/main.c +++ b/tools/marathon-sandbox/main.c @@ -15,64 +15,50 @@ #include #ifndef landlock_create_ruleset -static inline int landlock_create_ruleset( - const struct landlock_ruleset_attr *attr, size_t size, __u32 flags) -{ +static inline int landlock_create_ruleset(const struct landlock_ruleset_attr *attr, size_t size, + __u32 flags) { return syscall(__NR_landlock_create_ruleset, attr, size, flags); } #endif #ifndef landlock_add_rule static inline int landlock_add_rule(int ruleset_fd, enum landlock_rule_type rule_type, - const void *rule_attr, __u32 flags) -{ + const void *rule_attr, __u32 flags) { return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags); } #endif #ifndef landlock_restrict_self -static inline int landlock_restrict_self(int ruleset_fd, __u32 flags) -{ +static inline int landlock_restrict_self(int ruleset_fd, __u32 flags) { return syscall(__NR_landlock_restrict_self, ruleset_fd, flags); } #endif -#define ACCESS_FS_ROUGHLY_READ ( \ - LANDLOCK_ACCESS_FS_EXECUTE | \ - LANDLOCK_ACCESS_FS_READ_FILE | \ - LANDLOCK_ACCESS_FS_READ_DIR) - -#define ACCESS_FS_ROUGHLY_WRITE ( \ - LANDLOCK_ACCESS_FS_WRITE_FILE | \ - LANDLOCK_ACCESS_FS_REMOVE_DIR | \ - LANDLOCK_ACCESS_FS_REMOVE_FILE | \ - LANDLOCK_ACCESS_FS_MAKE_CHAR | \ - LANDLOCK_ACCESS_FS_MAKE_DIR | \ - LANDLOCK_ACCESS_FS_MAKE_REG | \ - LANDLOCK_ACCESS_FS_MAKE_SOCK | \ - LANDLOCK_ACCESS_FS_MAKE_FIFO | \ - LANDLOCK_ACCESS_FS_MAKE_BLOCK | \ - LANDLOCK_ACCESS_FS_MAKE_SYM | \ - LANDLOCK_ACCESS_FS_TRUNCATE) - -static int add_path_rule(int ruleset_fd, const char *path, __u64 allowed_access) -{ +#define ACCESS_FS_ROUGHLY_READ \ + (LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR) + +#define ACCESS_FS_ROUGHLY_WRITE \ + (LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_REMOVE_DIR | \ + LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_MAKE_CHAR | LANDLOCK_ACCESS_FS_MAKE_DIR | \ + LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_FIFO | \ + LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM | LANDLOCK_ACCESS_FS_TRUNCATE) + +static int add_path_rule(int ruleset_fd, const char *path, __u64 allowed_access) { struct landlock_path_beneath_attr path_beneath = { .allowed_access = allowed_access, }; - + path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC); if (path_beneath.parent_fd < 0) { return -1; } - + int ret = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0); close(path_beneath.parent_fd); return ret; } -static int setup_seccomp(void) -{ +static int setup_seccomp(void) { if (prctl(PR_GET_SECCOMP) == -1 && errno == EINVAL) { fprintf(stderr, "[marathon-sandbox] Seccomp not available\n"); return 0; @@ -149,53 +135,52 @@ static int setup_seccomp(void) __NR_open_by_handle_at, #endif }; - - const size_t num_blocked = sizeof(blocked_syscalls) / sizeof(blocked_syscalls[0]); - const size_t filter_size = 2 + (2 * num_blocked) + 1; - struct sock_filter *filter = calloc(filter_size, sizeof(struct sock_filter)); + + const size_t num_blocked = sizeof(blocked_syscalls) / sizeof(blocked_syscalls[0]); + const size_t filter_size = 2 + (2 * num_blocked) + 1; + struct sock_filter *filter = calloc(filter_size, sizeof(struct sock_filter)); if (!filter) { perror("[marathon-sandbox] Failed to allocate seccomp filter"); return -1; } - + size_t idx = 0; - - filter[idx++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, - offsetof(struct seccomp_data, nr)); - + + filter[idx++] = + (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)); + #if defined(__x86_64__) - filter[idx++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, - offsetof(struct seccomp_data, arch)); - filter[idx-1] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, - offsetof(struct seccomp_data, nr)); + filter[idx++] = + (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)); + filter[idx - 1] = + (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)); #endif - + for (size_t i = 0; i < num_blocked; i++) { - filter[idx++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, - blocked_syscalls[i], 0, 1); - filter[idx++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, - SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)); + filter[idx++] = + (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, blocked_syscalls[i], 0, 1); + filter[idx++] = (struct sock_filter)BPF_STMT( + BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)); } - + filter[idx++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); - + struct sock_fprog prog = { - .len = (unsigned short)idx, + .len = (unsigned short)idx, .filter = filter, }; - + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) { perror("[marathon-sandbox] Failed to apply seccomp filter"); free(filter); return -1; } - + free(filter); return 0; } -static int setup_landlock(void) -{ +static int setup_landlock(void) { int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); if (abi < 0) { if (errno == ENOSYS || errno == EOPNOTSUPP) { @@ -231,28 +216,28 @@ static int setup_landlock(void) add_path_rule(ruleset_fd, "/opt", ACCESS_FS_ROUGHLY_READ); add_path_rule(ruleset_fd, "/proc", ACCESS_FS_ROUGHLY_READ); add_path_rule(ruleset_fd, "/sys", ACCESS_FS_ROUGHLY_READ); - + add_path_rule(ruleset_fd, "/dev", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); add_path_rule(ruleset_fd, "/dev/dri", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); add_path_rule(ruleset_fd, "/dev/shm", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); - + add_path_rule(ruleset_fd, "/run/dbus", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); add_path_rule(ruleset_fd, "/run/user", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); - + const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); if (xdg_runtime) { add_path_rule(ruleset_fd, xdg_runtime, ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); } - + add_path_rule(ruleset_fd, "/var", ACCESS_FS_ROUGHLY_READ); add_path_rule(ruleset_fd, "/var/cache", ACCESS_FS_ROUGHLY_READ); add_path_rule(ruleset_fd, "/var/lib", ACCESS_FS_ROUGHLY_READ); - + const char *home = getenv("HOME"); if (home) { add_path_rule(ruleset_fd, home, ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); } - + add_path_rule(ruleset_fd, "/tmp", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); add_path_rule(ruleset_fd, "/var/tmp", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); @@ -261,19 +246,18 @@ static int setup_landlock(void) close(ruleset_fd); return -1; } - + if (landlock_restrict_self(ruleset_fd, 0)) { perror("[marathon-sandbox] Failed to enforce ruleset"); close(ruleset_fd); return -1; } - + close(ruleset_fd); return 0; } -int main(int argc, char *argv[]) -{ +int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s [args...]\n", argv[0]); return 1; From 57d28b9255394a813526310f20ee205e494f28ed Mon Sep 17 00:00:00 2001 From: patrickjquinn Date: Wed, 31 Dec 2025 14:06:35 +0000 Subject: [PATCH 4/4] feat: Enhance Landlock sandbox with network rules, ABI versioning, and granular filesystem permissions via environment variables. --- shell/src/applaunchservice.cpp | 58 +++----- shell/src/ipc/shellservices.cpp | 43 +----- shell/src/networkmanagercpp.cpp | 4 +- shell/src/securitymanager.cpp | 70 +--------- shell/src/wayland/waylandcompositor.cpp | 98 +++++++++---- shell/src/wayland/waylandcompositor.h | 2 +- tools/marathon-sandbox/main.c | 177 ++++++++++++++++++------ 7 files changed, 233 insertions(+), 219 deletions(-) diff --git a/shell/src/applaunchservice.cpp b/shell/src/applaunchservice.cpp index 7ab67d856..67514ff6f 100644 --- a/shell/src/applaunchservice.cpp +++ b/shell/src/applaunchservice.cpp @@ -38,7 +38,6 @@ void AppLaunchService::setCompositor(QObject *obj) { m_compositor = obj; - // Connect compositor signals in C++ (no more QML "pending app" glue). if (m_compositor) { #if defined(HAVE_WAYLAND) if (auto *wc = qobject_cast(m_compositor.data())) { @@ -106,9 +105,6 @@ void AppLaunchService::closeNativeApp(int surfaceId) { } QVariantMap AppLaunchService::resolveAppObject(const QVariant &app) const { - // Accept either a string appId or a JS object (QVariantMap / QJSValue / QObject*) - // IMPORTANT: Check map/object types before QString conversion; Qt can consider many variants - // "convertible" to QString, which would break object launches (e.g. quick actions). if (app.typeId() == QMetaType::QVariantMap) { QVariantMap out = app.toMap(); const QString appId = out.value("id").toString(); @@ -153,7 +149,6 @@ QVariantMap AppLaunchService::resolveAppObject(const QVariant &app) const { }; } - // Fallback: running task (externally launched) if (m_taskModel) { if (Task *t = m_taskModel->getTaskByAppId(appId)) { return { @@ -169,7 +164,6 @@ QVariantMap AppLaunchService::resolveAppObject(const QVariant &app) const { return {}; } - // If a QObject was passed (e.g. App* from QML), try reading properties. if (QObject *obj = app.value()) { QVariantMap out; const QVariant id = obj->property("id"); @@ -247,17 +241,13 @@ bool AppLaunchService::launchNativeApp(const QVariantMap &app, QObject *composit return false; } - // UI state (QML singleton) if (m_uiStore) invokeVoid(m_uiStore, "openApp", {appId, name, icon}); - // Show splash screen (no surface yet) invokeVoid(appWindowRef, "show", {appId, name, icon, QStringLiteral("native"), QVariant(), -1}); emit appLaunchProgress(appId, 50); - // Launch native app via compositor if (!exec.isEmpty()) { - // Track the exact command we pass into launchApp(), then correlate to pid/surface. PendingLaunch p; p.appId = appId; p.name = name; @@ -283,17 +273,13 @@ bool AppLaunchService::launchMarathonApp(const QVariantMap &app, QObject * /*com const QString icon = app.value("icon").toString(); const QString type = app.value("type").toString(); - // New architecture: run Marathon apps out-of-process via marathon-app-runner. - // They become Wayland clients, just like other native apps. - QObject *comp = m_compositor.data(); + QObject *comp = m_compositor.data(); if (!comp) { m_launchingApps.remove(appId); emit appLaunchFailed(appId, name, "Compositor not available"); return false; } - // Out-of-process apps are tracked via TaskModel once their surface arrives. - // If we already have a native task with a live surface, just bring it to foreground. if (m_taskModel) { if (Task *existing = m_taskModel->getTaskByAppId(appId)) { if (existing->appType() == "native" && existing->surfaceId() >= 0 && @@ -317,14 +303,11 @@ bool AppLaunchService::launchMarathonApp(const QVariantMap &app, QObject * /*com invokeVoid(m_uiStore, "openApp", {appId, name, icon}); emit appLaunchProgress(appId, 30); - // Show splash (no surface yet). We render this app as a Wayland-embedded client. invokeVoid(appWindowRef, "show", {appId, name, icon, QStringLiteral("native"), QVariant(), -1}); - emit appLaunchProgress(appId, 60); + emit appLaunchProgress(appId, 60); - // Launch the out-of-process runner as a Wayland client (connects to our compositor socket). - // Prefer the build-tree runner when developing; fall back to PATH lookup for installed setups. QString runnerPath; - const QDir shellBinDir(QCoreApplication::applicationDirPath()); // .../build/shell + const QDir shellBinDir(QCoreApplication::applicationDirPath()); const QString candidate = shellBinDir.filePath("../tools/marathon-app-runner/marathon-app-runner"); if (QFileInfo::exists(candidate)) { @@ -333,9 +316,18 @@ bool AppLaunchService::launchMarathonApp(const QVariantMap &app, QObject * /*com runnerPath = QStringLiteral("marathon-app-runner"); } + QVariantMap env; + QStringList permissions = app.value("permissions").toStringList(); + + if (permissions.contains("network")) { + env.insert("MARATHON_PERM_NETWORK", "1"); + } + if (permissions.contains("storage")) { + env.insert("MARATHON_PERM_STORAGE", "1"); + } + const QString cmd = QStringLiteral("%1 --app-id %2").arg(runnerPath, appId); - // Track the runner command for pid/surface correlation. PendingLaunch p; p.appId = appId; p.name = name; @@ -343,10 +335,11 @@ bool AppLaunchService::launchMarathonApp(const QVariantMap &app, QObject * /*com p.command = cmd; m_pendingByCommand.insert(cmd, p); - const bool ok = - QMetaObject::invokeMethod(comp, "launchApp", Qt::DirectConnection, Q_ARG(QString, cmd)); + const bool ok = QMetaObject::invokeMethod(comp, "launchApp", Qt::DirectConnection, + Q_ARG(QString, cmd), Q_ARG(QVariantMap, env)); if (!ok) - qWarning() << "[AppLaunchService] Failed to invoke launchApp(QString) on compositor"; + qWarning() << "[AppLaunchService] Failed to invoke launchApp(QString, QVariantMap) on " + "compositor"; m_launchingApps.remove(appId); emit appLaunchProgress(appId, 100); @@ -357,8 +350,6 @@ bool AppLaunchService::launchMarathonApp(const QVariantMap &app, QObject * /*com bool AppLaunchService::invokeVoid(QObject *obj, const char *method, const QVariantList &args) { if (!obj) return false; - // QMetaObject::invokeMethod requires the argument count to match the method signature. - // QML functions keep their declared parameter count, so we must pass the right number. switch (args.size()) { case 0: return QMetaObject::invokeMethod(obj, method, Qt::DirectConnection); case 1: @@ -447,7 +438,6 @@ void AppLaunchService::onCompositorAppClosed(qint64 pid) { if (m_appIdToPid.value(p.appId) == pid) m_appIdToPid.remove(p.appId); - // Close task when the process actually terminates (authoritative lifecycle). if (m_taskModel) { if (Task *t = m_taskModel->getTaskByAppId(p.appId)) { m_taskModel->closeTask(t->id()); @@ -476,7 +466,6 @@ bool AppLaunchService::isMarathonAppId(const QString &appId) const { } static QString runnerServiceNameForPid(qint64 pid) { - // D-Bus well-known name elements cannot start with a digit, so prefix the PID. return QStringLiteral("org.marathonos.AppRunner.pid%1").arg(pid); } @@ -518,7 +507,7 @@ static bool isDescendantPid(qint64 childPid, qint64 ancestorPid) { const qsizetype closeIdx = statLine.lastIndexOf(')'); if (closeIdx < 0) return false; - const QByteArray after = statLine.mid(closeIdx + 1).trimmed(); // "S ..." + const QByteArray after = statLine.mid(closeIdx + 1).trimmed(); const QList parts = after.split(' '); if (parts.size() < 2) return false; @@ -551,12 +540,9 @@ void AppLaunchService::onCompositorSurfaceCreated(QWaylandSurface *surface, int title = xdgSurface->toplevel()->title(); } - // Prefer passing the xdg surface object into QML so ShellSurfaceItem can access toplevel/configure. - // QWaylandSurface alone is not enough for sizing/configure in our QML glue. QObject *qmlSurfaceObj = xdgSurface ? static_cast(xdgSurface) : static_cast(surface); - // Case 1: match by PID (our launches) if (pid > 0) { auto it = m_activeByPid.find(pid); if (it != m_activeByPid.end()) { @@ -573,7 +559,6 @@ void AppLaunchService::onCompositorSurfaceCreated(QWaylandSurface *surface, int } } - // Ensure UI shows it (swap splash -> real surface) if (m_appWindow) invokeVoid(m_appWindow, "show", {p.appId, p.name, p.icon, QStringLiteral("native"), @@ -582,9 +567,6 @@ void AppLaunchService::onCompositorSurfaceCreated(QWaylandSurface *surface, int } } - // Case 1b (Flatpak/Sandbox correctness): the PID that connects to our Wayland socket may be - // a descendant of the PID we spawned (e.g. flatpak/bwrap wrapper). If so, attribute the - // surface to the original PendingLaunch instead of creating a "native-surface-*" task. #ifdef Q_OS_LINUX if (pid > 0 && !m_activeByPid.isEmpty()) { for (auto it = m_activeByPid.constBegin(); it != m_activeByPid.constEnd(); ++it) { @@ -598,7 +580,6 @@ void AppLaunchService::onCompositorSurfaceCreated(QWaylandSurface *surface, int qInfo() << "[AppLaunchService] Matched surfaceId" << surfaceId << "to child PID" << pid << "(ancestor PID" << parentPid << ") app" << p.appId; - // Record the real client pid for IPC enforcement (DBus sender pid → appId). registerPidForAppId(pid, p.appId); if (m_taskModel) { @@ -619,7 +600,6 @@ void AppLaunchService::onCompositorSurfaceCreated(QWaylandSurface *surface, int } #endif - // Case 2: secondary window for an existing task (match by xdg app_id) if (!xdgAppId.isEmpty() && m_taskModel) { if (Task *existing = m_taskModel->getTaskByAppId(xdgAppId)) { qInfo() << "[AppLaunchService] Secondary surface for existing appId" << xdgAppId @@ -629,7 +609,6 @@ void AppLaunchService::onCompositorSurfaceCreated(QWaylandSurface *surface, int } } - // Case 3: external launch — create a task based on appId/title (best-effort) QString effectiveAppId = xdgAppId; if (effectiveAppId.isEmpty() && !title.isEmpty()) effectiveAppId = @@ -642,7 +621,6 @@ void AppLaunchService::onCompositorSurfaceCreated(QWaylandSurface *surface, int QString appIcon = !xdgAppId.isEmpty() ? xdgAppId : QStringLiteral("application-x-executable"); if (m_appModel && !xdgAppId.isEmpty()) { if (App *a = m_appModel->getApp(xdgAppId)) { - // Prefer theme lookup by appId if present; otherwise use whatever AppModel provides. if (!a->icon().isEmpty()) appIcon = a->icon(); } diff --git a/shell/src/ipc/shellservices.cpp b/shell/src/ipc/shellservices.cpp index 05e92b117..e905c3aa9 100644 --- a/shell/src/ipc/shellservices.cpp +++ b/shell/src/ipc/shellservices.cpp @@ -66,10 +66,6 @@ static QString dbusCallerAppIdOrEmpty(const QDBusContext &ctx, AppLaunchService if (!mapped.isEmpty()) return mapped; - // Race-proofing for strict IPC: - // marathon-app-runner can legitimately call into the shell over DBus before we observed - // its pid via compositor signals. If (and only if) the caller is a direct child of this shell - // and looks like marathon-app-runner with a --app-id, we can safely self-heal the map. const qint64 shellPid = static_cast(QCoreApplication::applicationPid()); if (shellPid <= 0 || pid <= 0) return {}; @@ -81,7 +77,7 @@ static QString dbusCallerAppIdOrEmpty(const QDBusContext &ctx, AppLaunchService const qsizetype closeIdx = statLine.lastIndexOf(')'); if (closeIdx < 0) return {}; - const QByteArray after = statLine.mid(closeIdx + 1).trimmed(); // "S ..." + const QByteArray after = statLine.mid(closeIdx + 1).trimmed(); const QList parts = after.split(' '); if (parts.size() < 2) return {}; @@ -117,8 +113,6 @@ static QString dbusCallerAppIdOrEmpty(const QDBusContext &ctx, AppLaunchService return appId; } -// ---- SettingsObject ---- - SettingsObject::SettingsObject(SettingsManager *settings, MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) : QObject(parent) @@ -286,7 +280,6 @@ void SettingsObject::SetProperty(const QString &name, const QDBusVariant &value) return; const QVariant v = value.variant(); - // Map property name to typed setters (so signals + in-memory state stay consistent). if (name == "userScaleFactor") m_settings->setUserScaleFactor(v.toReal()); else if (name == "wallpaperPath") @@ -412,8 +405,6 @@ QString SettingsObject::AssetUrl(const QString &relativePath) { } QDBusVariant SettingsObject::Get(const QString &key, const QDBusVariant &defaultValue) { - // App-scoped keys (e.g. "clock/...") are allowed with storage permission and are isolated - // by enforcing the "appId/" prefix. if (isAppScopedKey(key)) { if (!requireStorage()) return QDBusVariant(); @@ -439,8 +430,6 @@ void SettingsObject::Set(const QString &key, const QDBusVariant &value) { } void SettingsObject::Sync() { - // Sync can be used by apps persisting their app-scoped keys. - // If the caller has either system or storage permission, allow it. if (!requireSystem()) { if (!requireStorage()) return; @@ -448,8 +437,6 @@ void SettingsObject::Sync() { m_settings->sync(); } -// ---- BluetoothObject ---- - BluetoothObject::BluetoothObject(BluetoothManager *bt, MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) : QObject(parent) @@ -501,8 +488,6 @@ QVariantList BluetoothObject::buildDevices(bool pairedOnly) const { for (QObject *d : list) { if (!d) continue; - // IMPORTANT: wrap complex nested payloads in QDBusVariant for reliable QtDBus marshalling. - // (List-of-map inside a{sv} can otherwise arrive empty on the client.) out.push_back(QVariant::fromValue(QDBusVariant(QVariantMap{ {"address", d->property("address").toString()}, {"name", d->property("name").toString()}, @@ -586,8 +571,6 @@ void BluetoothObject::CancelPairing(const QString &address) { m_bt->cancelPairing(address); } -// ---- DisplayObject ---- - DisplayObject::DisplayObject(DisplayManagerCpp *display, MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) : QObject(parent) @@ -695,8 +678,6 @@ void DisplayObject::SetScreenState(bool on) { m_display->setScreenState(on); } -// ---- PowerObject ---- - PowerObject::PowerObject(PowerManagerCpp *power, MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) : QObject(parent) @@ -781,8 +762,6 @@ void PowerObject::Shutdown() { m_power->shutdown(); } -// ---- AudioObject ---- - static QVariantList audioStreamsToVariantList(AudioManagerCpp *audio) { QVariantList out; if (!audio || !audio->streams()) @@ -861,7 +840,6 @@ QVariantMap AudioObject::GetState() { void AudioObject::SetVolume(double volume) { if (!requireSystem()) return; - // Use policy controller if present to keep SettingsManager.systemVolume in sync. if (m_policy) { m_policy->setMasterVolume(volume); } else { @@ -960,8 +938,6 @@ void AudioObject::VibratePattern(const QVariantList &pattern) { m_policy->vibratePattern(pattern); } -// ---- NetworkObject ---- - NetworkObject::NetworkObject(NetworkManagerCpp *network, MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) : QObject(parent) @@ -1012,7 +988,6 @@ bool NetworkObject::requireNetwork() { } QVariantMap NetworkObject::buildState() const { - // IMPORTANT: wrap complex nested payloads in QDBusVariant for reliable QtDBus marshalling. QVariantList wrappedNetworks; const QVariantList nets = m_network->availableNetworks(); wrappedNetworks.reserve(nets.size()); @@ -1097,14 +1072,11 @@ void NetworkObject::StopHotspot() { } bool NetworkObject::IsHotspotActive() const { - // Pure read: still enforce auth to avoid leaking state. if (!const_cast(this)->requireNetwork()) return false; return m_network->isHotspotActive(); } -// ---- MediaLibraryObject ---- - MediaLibraryObject::MediaLibraryObject(MediaLibraryManager *media, MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) @@ -1160,7 +1132,6 @@ QVariantList MediaLibraryObject::Albums() { } bool MediaLibraryObject::IsScanning() const { - // Strict for now: require permission even for read. if (!const_cast(this)->requireStorage()) return false; return m_media->isScanning(); @@ -1244,8 +1215,6 @@ void MediaLibraryObject::DeleteMedia(int mediaId) { m_media->deleteMedia(mediaId); } -// ---- PermissionsObject ---- - PermissionsObject::PermissionsObject(MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) : QObject(parent) @@ -1293,8 +1262,6 @@ void PermissionsObject::SetPermission(const QString &appId, const QString &permi m_permissions->setPermission(caller, permission, granted, remember); } -// ---- ContactsObject ---- - ContactsObject::ContactsObject(ContactsManager *contacts, MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) : QObject(parent) @@ -1373,8 +1340,6 @@ void ContactsObject::DeleteContact(int id) { m_contacts->deleteContact(id); } -// ---- CallHistoryObject ---- - CallHistoryObject::CallHistoryObject(CallHistoryManager *callHistory, MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) @@ -1401,7 +1366,6 @@ bool CallHistoryObject::requirePhone() { sendErrorReply(QDBusError::AccessDenied, "Unknown caller"); return false; } - // Compatibility: phone apps may request "phone" (legacy) instead of "telephony". if (!hasAnyPermission(m_permissions, caller, {"telephony", "phone"})) { sendErrorReply(QDBusError::AccessDenied, "Missing permission: telephony/phone"); return false; @@ -1446,8 +1410,6 @@ void CallHistoryObject::ClearHistory() { m_callHistory->clearHistory(); } -// ---- TelephonyObject ---- - TelephonyObject::TelephonyObject(TelephonyService *telephony, MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) @@ -1489,7 +1451,6 @@ bool TelephonyObject::requirePhone() { } QString TelephonyObject::CallState() const { - // Allow read without permission? keep strict for now. if (!const_cast(this)->requirePhone()) return {}; return m_telephony->callState(); @@ -1531,8 +1492,6 @@ void TelephonyObject::SendDTMF(const QString &digit) { m_telephony->sendDTMF(digit); } -// ---- SmsObject ---- - SmsObject::SmsObject(SMSService *sms, MarathonPermissionManager *permissions, AppLaunchService *launchService, QObject *parent) : QObject(parent) diff --git a/shell/src/networkmanagercpp.cpp b/shell/src/networkmanagercpp.cpp index 5690b31de..6b186068c 100644 --- a/shell/src/networkmanagercpp.cpp +++ b/shell/src/networkmanagercpp.cpp @@ -474,7 +474,7 @@ void NetworkManagerCpp::scanAccessPoints() { } } - qWarning() << "[NetworkManagerCpp] Found" << accessPoints.size() << "access points"; + qDebug() << "[NetworkManagerCpp] Found" << accessPoints.size() << "access points"; m_availableNetworks.clear(); @@ -494,7 +494,7 @@ void NetworkManagerCpp::scanAccessPoints() { return am.value("strength").toInt() > bm.value("strength").toInt(); }); - qWarning() << "[NetworkManagerCpp] Processed" << m_availableNetworks.size() << "networks"; + qDebug() << "[NetworkManagerCpp] Processed" << m_availableNetworks.size() << "networks"; emit availableNetworksChanged(); } diff --git a/shell/src/securitymanager.cpp b/shell/src/securitymanager.cpp index d945c656d..736755565 100644 --- a/shell/src/securitymanager.cpp +++ b/shell/src/securitymanager.cpp @@ -27,28 +27,22 @@ SecurityManager::SecurityManager(QObject *parent) , m_fprintdAuthInProgress(false) , m_passwordAuthWatcher(new QFutureWatcher(this)) , m_quickPINAuthWatcher(new QFutureWatcher(this)) { - // Initialize security event logging SecurityLogger::initialize(); qDebug() << "[SecurityManager] Initializing authentication system"; - // Initialize fprintd initFingerprintDevice(); - // Check for existing Quick PIN m_hasQuickPIN = !retrieveQuickPIN().isEmpty(); - // Set up lockout timer (check every second) m_lockoutTimer->setInterval(1000); connect(m_lockoutTimer, &QTimer::timeout, this, &SecurityManager::checkLockoutTimer); - // Set up async authentication watchers connect(m_passwordAuthWatcher, &QFutureWatcher::finished, this, &SecurityManager::onPasswordAuthFinished); connect(m_quickPINAuthWatcher, &QFutureWatcher::finished, this, &SecurityManager::onQuickPINAuthFinished); - // Initial lockout status check updateLockoutStatus(); qDebug() << "[SecurityManager] Fingerprint available:" << m_fingerprintAvailable; @@ -65,10 +59,6 @@ SecurityManager::~SecurityManager() { } } -// ============================================================================ -// Property Setters -// ============================================================================ - void SecurityManager::setAuthMode(AuthMode mode) { if (m_authMode != mode) { m_authMode = mode; @@ -86,10 +76,6 @@ int SecurityManager::lockoutSecondsRemaining() const { return qMax(0, static_cast(msecs / 1000)); } -// ============================================================================ -// PAM Authentication -// ============================================================================ - int SecurityManager::pamConversationCallback(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) { if (num_msg <= 0 || !msg || !resp || !appdata_ptr) { @@ -105,8 +91,8 @@ int SecurityManager::pamConversationCallback(int num_msg, const struct pam_messa for (int i = 0; i < num_msg; i++) { switch (msg[i]->msg_style) { - case PAM_PROMPT_ECHO_OFF: // Password prompt - case PAM_PROMPT_ECHO_ON: // Username or other prompt + case PAM_PROMPT_ECHO_OFF: + case PAM_PROMPT_ECHO_ON: (*resp)[i].resp = strdup(password->toUtf8().constData()); (*resp)[i].resp_retcode = 0; break; @@ -140,7 +126,6 @@ bool SecurityManager::authenticateViaPAM(const QString &password) { m_currentPassword = password; - // Start PAM authentication int ret = pam_start("marathon-shell", username.toUtf8().constData(), &conv, &pamh); if (ret != PAM_SUCCESS) { qWarning() << "[SecurityManager] PAM start failed:" << pam_strerror(pamh, ret); @@ -148,7 +133,6 @@ bool SecurityManager::authenticateViaPAM(const QString &password) { return false; } - // Authenticate ret = pam_authenticate(pamh, PAM_SILENT); if (ret != PAM_SUCCESS) { qWarning() << "[SecurityManager] PAM authentication failed:" << pam_strerror(pamh, ret); @@ -157,7 +141,6 @@ bool SecurityManager::authenticateViaPAM(const QString &password) { return false; } - // Check account validity ret = pam_acct_mgmt(pamh, PAM_SILENT); if (ret != PAM_SUCCESS) { qWarning() << "[SecurityManager] PAM account management failed:" << pam_strerror(pamh, ret); @@ -166,7 +149,6 @@ bool SecurityManager::authenticateViaPAM(const QString &password) { return false; } - // Success pam_end(pamh, PAM_SUCCESS); m_currentPassword.clear(); @@ -177,7 +159,6 @@ bool SecurityManager::authenticateViaPAM(const QString &password) { void SecurityManager::authenticatePassword(const QString &password) { qDebug() << "[SecurityManager] Password authentication requested"; - // Check lockout status if (m_isLockedOut) { int remaining = lockoutSecondsRemaining(); emit authenticationFailed( @@ -190,10 +171,8 @@ void SecurityManager::authenticatePassword(const QString &password) { return; } - // Store password for async callback m_currentPassword = password; - // Run PAM authentication in background thread qDebug() << "[SecurityManager] Starting async PAM authentication"; QFuture future = QtConcurrent::run([this, password]() { return authenticateViaPAM(password); }); @@ -230,10 +209,6 @@ void SecurityManager::onPasswordAuthFinished() { } } -// ============================================================================ -// Quick PIN -// ============================================================================ - QByteArray SecurityManager::generateSalt(int length) { QByteArray salt; salt.resize(length); @@ -342,7 +317,6 @@ bool SecurityManager::verifyQuickPIN(const QString &pin) { void SecurityManager::authenticateQuickPIN(const QString &pin) { qDebug() << "[SecurityManager] Quick PIN authentication requested"; - // Check lockout status if (m_isLockedOut) { int remaining = lockoutSecondsRemaining(); emit authenticationFailed( @@ -355,7 +329,6 @@ void SecurityManager::authenticateQuickPIN(const QString &pin) { return; } - // Run PIN verification in background thread (for consistency) qDebug() << "[SecurityManager] Starting async Quick PIN verification"; QFuture future = QtConcurrent::run([this, pin]() { return verifyQuickPIN(pin); }); @@ -393,13 +366,11 @@ void SecurityManager::onQuickPINAuthFinished() { void SecurityManager::setQuickPIN(const QString &pin, const QString &systemPassword) { qDebug() << "[SecurityManager] Setting Quick PIN (requires system password verification)"; - // Verify system password first via PAM if (!authenticateViaPAM(systemPassword)) { emit authenticationFailed("System password incorrect. Cannot set Quick PIN."); return; } - // Store the Quick PIN if (storeQuickPIN(pin)) { m_hasQuickPIN = true; emit quickPINChanged(); @@ -412,7 +383,6 @@ void SecurityManager::setQuickPIN(const QString &pin, const QString &systemPassw void SecurityManager::removeQuickPIN(const QString &systemPassword) { qDebug() << "[SecurityManager] Removing Quick PIN (requires system password verification)"; - // Verify system password first via PAM if (!authenticateViaPAM(systemPassword)) { emit authenticationFailed("System password incorrect. Cannot remove Quick PIN."); return; @@ -424,14 +394,9 @@ void SecurityManager::removeQuickPIN(const QString &systemPassword) { qDebug() << "[SecurityManager] Quick PIN removed successfully"; } -// ============================================================================ -// Fingerprint Authentication (fprintd D-Bus) -// ============================================================================ - void SecurityManager::initFingerprintDevice() { qDebug() << "[SecurityManager] Initializing fprintd device"; - // Check if fprintd service is available QDBusConnection systemBus = QDBusConnection::systemBus(); if (!systemBus.interface()->isServiceRegistered("net.reactivated.Fprint")) { qDebug() << "[SecurityManager] fprintd service not available"; @@ -439,7 +404,6 @@ void SecurityManager::initFingerprintDevice() { return; } - // Create manager interface m_fprintdManager = new QDBusInterface("net.reactivated.Fprint", "/net/reactivated/Fprint/Manager", "net.reactivated.Fprint.Manager", systemBus, this); @@ -453,7 +417,6 @@ void SecurityManager::initFingerprintDevice() { return; } - // Get default device QDBusReply reply = m_fprintdManager->call("GetDefaultDevice"); if (!reply.isValid()) { qDebug() << "[SecurityManager] No fingerprint device available:" << reply.error().message(); @@ -464,7 +427,6 @@ void SecurityManager::initFingerprintDevice() { QString devicePath = reply.value().path(); qDebug() << "[SecurityManager] Found fingerprint device:" << devicePath; - // Create device interface m_fprintdDevice = new QDBusInterface("net.reactivated.Fprint", devicePath, "net.reactivated.Fprint.Device", systemBus, this); @@ -477,11 +439,9 @@ void SecurityManager::initFingerprintDevice() { return; } - // Connect to VerifyStatus signal systemBus.connect("net.reactivated.Fprint", devicePath, "net.reactivated.Fprint.Device", "VerifyStatus", this, SLOT(onFingerprintVerifyStatus(QString, bool))); - // Check if fingerprint is enrolled checkFingerprintEnrollment(); } @@ -509,7 +469,7 @@ bool SecurityManager::isBiometricEnrolled(BiometricType type) const { if (type == Fingerprint) { return m_fingerprintAvailable; } - return false; // Face recognition not implemented yet + return false; } void SecurityManager::authenticateBiometric(BiometricType type) { @@ -518,7 +478,6 @@ void SecurityManager::authenticateBiometric(BiometricType type) { return; } - // Check lockout status if (m_isLockedOut) { int remaining = lockoutSecondsRemaining(); emit authenticationFailed( @@ -541,7 +500,6 @@ void SecurityManager::startFingerprintAuth() { qDebug() << "[SecurityManager] Starting fingerprint authentication"; - // Claim the device QDBusReply claimReply = m_fprintdDevice->call("Claim", getCurrentUsername()); if (!claimReply.isValid()) { qWarning() << "[SecurityManager] Failed to claim fingerprint device:" @@ -550,7 +508,6 @@ void SecurityManager::startFingerprintAuth() { return; } - // Start verification QDBusReply verifyReply = m_fprintdDevice->call("VerifyStart", "any"); if (!verifyReply.isValid()) { qWarning() << "[SecurityManager] Failed to start fingerprint verification:" @@ -609,12 +566,7 @@ void SecurityManager::onFingerprintVerifyStatus(const QString &result, bool done } } -// ============================================================================ -// Rate Limiting and Lockout -// ============================================================================ - void SecurityManager::updateLockoutStatus() { - // Query pam_faillock for current status int attempts = queryFaillockAttempts(); if (attempts != m_failedAttempts) { @@ -622,11 +574,10 @@ void SecurityManager::updateLockoutStatus() { emit failedAttemptsChanged(); } - // Simple lockout: 5 attempts = 5 minutes if (m_failedAttempts >= 5) { if (!m_isLockedOut) { m_isLockedOut = true; - m_lockoutUntil = QDateTime::currentDateTime().addSecs(300); // 5 minutes + m_lockoutUntil = QDateTime::currentDateTime().addSecs(300); m_lockoutTimer->start(); emit lockoutStateChanged(); qWarning() << "[SecurityManager] Account locked out for 5 minutes"; @@ -635,7 +586,6 @@ void SecurityManager::updateLockoutStatus() { } int SecurityManager::queryFaillockAttempts() { - // Query faillock command for current user QString username = getCurrentUsername(); QProcess process; process.start("faillock", QStringList() << "--user" << username); @@ -643,10 +593,7 @@ int SecurityManager::queryFaillockAttempts() { QString output = process.readAllStandardOutput(); - // Parse output for failure count - // Example: "When Type Source Valid" - // "2024-11-09 12:34:56 TTY marathon-shell V" - int count = 0; + int count = 0; for (const QString &line : output.split('\n')) { if (line.contains("marathon-shell")) { count++; @@ -667,7 +614,6 @@ void SecurityManager::recordFailedAttempt() { void SecurityManager::resetFailedAttempts() { if (m_failedAttempts > 0) { - // Reset faillock for user QString username = getCurrentUsername(); QProcess::execute("faillock", QStringList() << "--user" << username << "--reset"); @@ -698,15 +644,11 @@ void SecurityManager::checkLockoutTimer() { emit lockoutStateChanged(); qDebug() << "[SecurityManager] Lockout period expired"; } else { - emit lockoutStateChanged(); // Update UI with remaining time + emit lockoutStateChanged(); } } } -// ============================================================================ -// Helper Methods -// ============================================================================ - QString SecurityManager::getCurrentUsername() const { uid_t uid = getuid(); struct passwd *pw = getpwuid(uid); diff --git a/shell/src/wayland/waylandcompositor.cpp b/shell/src/wayland/waylandcompositor.cpp index 1b1847c66..84a21f1cc 100644 --- a/shell/src/wayland/waylandcompositor.cpp +++ b/shell/src/wayland/waylandcompositor.cpp @@ -17,13 +17,18 @@ static bool envBool(const char *name, bool defaultValue) { const QByteArray raw = qgetenv(name); - if (raw.isEmpty()) + if (raw.isEmpty()) { return defaultValue; - const QByteArray v = raw.trimmed().toLower(); - if (v == "1" || v == "true" || v == "yes" || v == "on") + } + const QByteArray valueString = raw.trimmed().toLower(); + if (valueString == "1" || valueString == "true" || valueString == "yes" || + valueString == "on") { return true; - if (v == "0" || v == "false" || v == "no" || v == "off") + } + if (valueString == "0" || valueString == "false" || valueString == "no" || + valueString == "off") { return false; + } return defaultValue; } @@ -146,20 +151,31 @@ QQmlListProperty WaylandCompositor::surfaces() { return QQmlListProperty(this, &m_surfaces); } -void WaylandCompositor::launchApp(const QString &command) { - if (wlVerbose()) { +void WaylandCompositor::launchApp(const QString &command, const QVariantMap &extraEnv) { + if (command.isEmpty()) { qDebug() << "[WaylandCompositor] Launching app:" << command; qDebug() << "[WaylandCompositor] Socket name:" << socketName(); qDebug() << "[WaylandCompositor] XDG_RUNTIME_DIR:" << qgetenv("XDG_RUNTIME_DIR"); } QString actualCommand = command; - const bool isFlatpak = command.startsWith("FLATPAK:"); - const bool isSnap = command.startsWith("SNAP:"); + bool isFlatpak = command.startsWith("FLATPAK:"); + bool isSnap = command.startsWith("SNAP:"); QStringList execPartsOverride; if (isFlatpak) { - actualCommand = command.mid(8); + actualCommand = command.mid(8); + } else if (command.startsWith("flatpak run")) { + isFlatpak = true; + } + + if (isSnap) { + actualCommand = command.mid(5); + } else if (command.startsWith("snap run")) { + isSnap = true; + } + + if (isFlatpak) { const QStringList parts = QProcess::splitCommand(actualCommand); if (parts.size() >= 2 && parts.at(0) == "flatpak" && parts.at(1) == "run") { QStringList newParts; @@ -181,15 +197,17 @@ void WaylandCompositor::launchApp(const QString &command) { } if (isSnap) { - actualCommand = command.mid(5); qInfo() << "[WaylandCompositor] Snap app - wayland interface should be connected"; qInfo() << "[WaylandCompositor] Run 'snap connections APP' to verify wayland interface"; } QProcess *process = new QProcess(this); - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - QString runtimeDir = QString::fromLocal8Bit(qgetenv("XDG_RUNTIME_DIR")); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + for (auto it = extraEnv.constBegin(); it != extraEnv.constEnd(); ++it) { + env.insert(it.key(), it.value().toString()); + } + QString runtimeDir = QString::fromLocal8Bit(qgetenv("XDG_RUNTIME_DIR")); if (runtimeDir.isEmpty()) { qWarning() << "[WaylandCompositor] XDG_RUNTIME_DIR not set! Apps may fail to connect."; @@ -199,10 +217,11 @@ void WaylandCompositor::launchApp(const QString &command) { env.remove("WAYLAND_DISPLAY"); env.remove("DISPLAY"); + const bool isRunner = actualCommand.contains("marathon-app-runner"); + qint64 timestamp = QDateTime::currentMSecsSinceEpoch(); uint commandHash = qHash(actualCommand); - const bool isRunner = actualCommand.contains("marathon-app-runner"); if (!isFlatpak && !isRunner) { QDir tmpDir("/tmp/marathon-apps"); if (!tmpDir.exists()) { @@ -250,12 +269,12 @@ void WaylandCompositor::launchApp(const QString &command) { { const QByteArray debugEnv = qgetenv("MARATHON_DEBUG").trimmed().toLower(); const bool debug = (debugEnv == "1" || debugEnv == "true"); - if (debug) + if (debug) { env.insert("QT_FORCE_STDERR_LOGGING", "1"); + } } const bool forceShm = envBool("MARATHON_FORCE_WAYLAND_SHM", false); if (!env.contains("QT_WAYLAND_CLIENT_BUFFER_INTEGRATION")) { - // wayland-egl for hardware accelerated rendering env.insert("QT_WAYLAND_CLIENT_BUFFER_INTEGRATION", forceShm ? "shm" : "wayland-egl"); } @@ -366,8 +385,9 @@ void WaylandCompositor::launchApp(const QString &command) { m_processes[process] = actualCommand; - if (wlVerbose()) + if (wlVerbose()) { qDebug() << "[WaylandCompositor] Starting process:" << actualCommand; + } const QStringList parts = execPartsOverride.isEmpty() ? QProcess::splitCommand(actualCommand) : execPartsOverride; @@ -388,14 +408,40 @@ void WaylandCompositor::launchApp(const QString &command) { } }); - const bool useSandbox = !isFlatpak && !isSnap && envBool("MARATHON_ENABLE_SANDBOX", true); + const bool useSandbox = !isFlatpak && !isSnap && envBool("MARATHON_ENABLE_SANDBOX", true); + + const QString dbusConfigPath = runtimeDir + "/marathon-dbus-restricted.conf"; + if (useSandbox && !isRunner) { + QFile f(dbusConfigPath); + if (f.open(QIODevice::WriteOnly)) { + QTextStream out(&f); + out << "\n"; + out << " session\n"; + out << " unix:tmpdir=/tmp\n"; + out << " \n"; + out << " \n"; + out << " \n"; + out << " \n"; + out << " \n"; + out << "\n"; + f.close(); + } + } if (!needsShell) { if (useSandbox) { - QStringList sandboxArgs; - sandboxArgs << program << args; - process->setProgram("marathon-sandbox"); - process->setArguments(sandboxArgs); + QStringList finalArgs; + QString finalProgram; + if (!isRunner) { + finalProgram = "dbus-run-session"; + finalArgs << "--config-file" << dbusConfigPath << "--" << "marathon-sandbox" + << program << args; + } else { + finalProgram = "marathon-sandbox"; + finalArgs << program << args; + } + process->setProgram(finalProgram); + process->setArguments(finalArgs); } else { process->setProgram(program); process->setArguments(args); @@ -403,7 +449,13 @@ void WaylandCompositor::launchApp(const QString &command) { process->start(); } else { if (useSandbox) { - process->start("marathon-sandbox", {"/bin/sh", "-c", actualCommand}); + if (!isRunner) { + process->start("dbus-run-session", + {"--config-file", dbusConfigPath, "--", "marathon-sandbox", + "/bin/sh", "-c", actualCommand}); + } else { + process->start("marathon-sandbox", {"/bin/sh", "-c", actualCommand}); + } } else { process->start("/bin/sh", {"-c", actualCommand}); } @@ -458,7 +510,6 @@ void WaylandCompositor::closeWindow(int surfaceId) { return; } - // Mark this process as being closed by the shell (avoid mislabeling signal-based termination as a crash). targetProcess->setProperty("marathonCloseRequested", true); QPointer safeProcessPtr(targetProcess); @@ -881,7 +932,6 @@ void WaylandCompositor::handleProcessError(QProcess::ProcessError error) { const QString output = outputTail + QString::fromLocal8Bit(process->readAllStandardOutput()); const QString err = errTail + QString::fromLocal8Bit(process->readAllStandardError()); - // Always dump recent output on errors (even if MARATHON_WL_VERBOSE=0). if (!output.trimmed().isEmpty()) qWarning() << "[WaylandCompositor] stdout tail for" << command << ":\n" << output.trimmed(); if (!err.trimmed().isEmpty()) diff --git a/shell/src/wayland/waylandcompositor.h b/shell/src/wayland/waylandcompositor.h index 39f926ff0..1d32fa96e 100644 --- a/shell/src/wayland/waylandcompositor.h +++ b/shell/src/wayland/waylandcompositor.h @@ -33,7 +33,7 @@ class WaylandCompositor : public QWaylandCompositor { QWaylandQuickOutput *output() const { return m_output; } - Q_INVOKABLE void launchApp(const QString &command); + Q_INVOKABLE void launchApp(const QString &command, const QVariantMap &env = {}); Q_INVOKABLE void closeWindow(int surfaceId); Q_INVOKABLE void activateSurface(int surfaceId); Q_INVOKABLE QObject *getSurfaceById(int surfaceId); diff --git a/tools/marathon-sandbox/main.c b/tools/marathon-sandbox/main.c index 11b7a98bf..fc471ecf1 100644 --- a/tools/marathon-sandbox/main.c +++ b/tools/marathon-sandbox/main.c @@ -1,18 +1,20 @@ #define _GNU_SOURCE #include #include +#include +#include +#include #include #include -#include -#include #include #include #include #include #include +#include #include -#include #include +#include #ifndef landlock_create_ruleset static inline int landlock_create_ruleset(const struct landlock_ruleset_attr *attr, size_t size, @@ -34,6 +36,10 @@ static inline int landlock_restrict_self(int ruleset_fd, __u32 flags) { } #endif +#define ACCESS_FILE \ + (LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_READ_FILE | \ + LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV) + #define ACCESS_FS_ROUGHLY_READ \ (LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR) @@ -41,21 +47,46 @@ static inline int landlock_restrict_self(int ruleset_fd, __u32 flags) { (LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_REMOVE_DIR | \ LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_MAKE_CHAR | LANDLOCK_ACCESS_FS_MAKE_DIR | \ LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_FIFO | \ - LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM | LANDLOCK_ACCESS_FS_TRUNCATE) + LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM | LANDLOCK_ACCESS_FS_REFER | \ + LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV) static int add_path_rule(int ruleset_fd, const char *path, __u64 allowed_access) { + struct stat statbuf; struct landlock_path_beneath_attr path_beneath = { .allowed_access = allowed_access, }; path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC); if (path_beneath.parent_fd < 0) { - return -1; + return 0; + } + + if (fstat(path_beneath.parent_fd, &statbuf)) { + close(path_beneath.parent_fd); + return 0; + } + + if (!S_ISDIR(statbuf.st_mode)) { + path_beneath.allowed_access &= ACCESS_FILE; } int ret = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0); close(path_beneath.parent_fd); - return ret; + return ret == 0 ? 0 : -1; +} + +static int add_net_port_rule(int ruleset_fd, __u16 port, __u64 allowed_access) { + struct landlock_net_port_attr net_port = { + .allowed_access = allowed_access, + .port = port, + }; + return landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, &net_port, 0); +} + +static void add_standard_net_rules(int ruleset_fd) { + add_net_port_rule(ruleset_fd, 80, LANDLOCK_ACCESS_NET_CONNECT_TCP); + add_net_port_rule(ruleset_fd, 443, LANDLOCK_ACCESS_NET_CONNECT_TCP); + add_net_port_rule(ruleset_fd, 53, LANDLOCK_ACCESS_NET_CONNECT_TCP); } static int setup_seccomp(void) { @@ -180,7 +211,7 @@ static int setup_seccomp(void) { return 0; } -static int setup_landlock(void) { +static int setup_landlock(int has_network_permission, int has_storage_permission) { int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); if (abi < 0) { if (errno == ENOSYS || errno == EOPNOTSUPP) { @@ -190,61 +221,93 @@ static int setup_landlock(void) { return -1; } - struct landlock_ruleset_attr ruleset_attr = { - .handled_access_fs = ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE, - }; - - if (abi < 2) { - ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER; + __u64 access_fs_rw = ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE; + __u64 access_net = LANDLOCK_ACCESS_NET_BIND_TCP | LANDLOCK_ACCESS_NET_CONNECT_TCP; + __u64 scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL; + int has_net_rules = 0; + int has_scoping = 0; + + switch (abi) { + case 1: access_fs_rw &= ~LANDLOCK_ACCESS_FS_REFER; __attribute__((fallthrough)); + case 2: access_fs_rw &= ~LANDLOCK_ACCESS_FS_TRUNCATE; __attribute__((fallthrough)); + case 3: access_net = 0; __attribute__((fallthrough)); + case 4: access_fs_rw &= ~LANDLOCK_ACCESS_FS_IOCTL_DEV; __attribute__((fallthrough)); + case 5: scoped = 0; __attribute__((fallthrough)); + default: break; } - if (abi < 3) { - ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE; + + if (has_network_permission) { + access_net = 0; } + has_net_rules = (access_net != 0); + has_scoping = (scoped != 0); + + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = access_fs_rw, + .handled_access_net = access_net, + .scoped = scoped, + }; + int ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); if (ruleset_fd < 0) { perror("[marathon-sandbox] Failed to create ruleset"); return -1; } - add_path_rule(ruleset_fd, "/usr", ACCESS_FS_ROUGHLY_READ); - add_path_rule(ruleset_fd, "/lib", ACCESS_FS_ROUGHLY_READ); - add_path_rule(ruleset_fd, "/lib64", ACCESS_FS_ROUGHLY_READ); - add_path_rule(ruleset_fd, "/bin", ACCESS_FS_ROUGHLY_READ); - add_path_rule(ruleset_fd, "/sbin", ACCESS_FS_ROUGHLY_READ); - add_path_rule(ruleset_fd, "/etc", ACCESS_FS_ROUGHLY_READ); - add_path_rule(ruleset_fd, "/opt", ACCESS_FS_ROUGHLY_READ); - add_path_rule(ruleset_fd, "/proc", ACCESS_FS_ROUGHLY_READ); - add_path_rule(ruleset_fd, "/sys", ACCESS_FS_ROUGHLY_READ); + __u64 access_ro = ACCESS_FS_ROUGHLY_READ & access_fs_rw; + __u64 access_rw = access_fs_rw; + + add_path_rule(ruleset_fd, "/usr", access_ro); + add_path_rule(ruleset_fd, "/lib", access_ro); + add_path_rule(ruleset_fd, "/lib64", access_ro); + add_path_rule(ruleset_fd, "/bin", access_ro); + add_path_rule(ruleset_fd, "/sbin", access_ro); + add_path_rule(ruleset_fd, "/etc", access_ro); + add_path_rule(ruleset_fd, "/opt", access_ro); + add_path_rule(ruleset_fd, "/proc", access_ro); + add_path_rule(ruleset_fd, "/sys", access_ro); + add_path_rule(ruleset_fd, "/var/cache", access_ro); + add_path_rule(ruleset_fd, "/var/lib", access_ro); - add_path_rule(ruleset_fd, "/dev", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); - add_path_rule(ruleset_fd, "/dev/dri", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); - add_path_rule(ruleset_fd, "/dev/shm", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + add_path_rule(ruleset_fd, "/dev", access_rw); + add_path_rule(ruleset_fd, "/dev/dri", access_rw); + add_path_rule(ruleset_fd, "/dev/shm", access_rw); - add_path_rule(ruleset_fd, "/run/dbus", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); - add_path_rule(ruleset_fd, "/run/user", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + add_path_rule(ruleset_fd, "/run/dbus", access_rw); + add_path_rule(ruleset_fd, "/run/user", access_rw); const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); if (xdg_runtime) { - add_path_rule(ruleset_fd, xdg_runtime, ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + add_path_rule(ruleset_fd, xdg_runtime, access_rw); } - add_path_rule(ruleset_fd, "/var", ACCESS_FS_ROUGHLY_READ); - add_path_rule(ruleset_fd, "/var/cache", ACCESS_FS_ROUGHLY_READ); - add_path_rule(ruleset_fd, "/var/lib", ACCESS_FS_ROUGHLY_READ); - const char *home = getenv("HOME"); if (home) { - add_path_rule(ruleset_fd, home, ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + if (has_storage_permission) { + add_path_rule(ruleset_fd, home, access_rw); + } else { + add_path_rule(ruleset_fd, home, access_ro); + + char config_path[PATH_MAX]; + snprintf(config_path, sizeof(config_path), "%s/.config", home); + add_path_rule(ruleset_fd, config_path, access_rw); + + char cache_path[PATH_MAX]; + snprintf(cache_path, sizeof(cache_path), "%s/.cache", home); + add_path_rule(ruleset_fd, cache_path, access_rw); + + char local_path[PATH_MAX]; + snprintf(local_path, sizeof(local_path), "%s/.local/share", home); + add_path_rule(ruleset_fd, local_path, access_rw); + } } - add_path_rule(ruleset_fd, "/tmp", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); - add_path_rule(ruleset_fd, "/var/tmp", ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE); + add_path_rule(ruleset_fd, "/tmp", access_rw); + add_path_rule(ruleset_fd, "/var/tmp", access_rw); - if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { - perror("[marathon-sandbox] Failed to set no_new_privs"); - close(ruleset_fd); - return -1; + if (has_net_rules) { + add_standard_net_rules(ruleset_fd); } if (landlock_restrict_self(ruleset_fd, 0)) { @@ -254,25 +317,47 @@ static int setup_landlock(void) { } close(ruleset_fd); + + syslog(LOG_INFO, "[SECURITY] Landlock sandbox active: abi=%d net=%d storage=%d scoped=%d", abi, + has_network_permission, has_storage_permission, has_scoping); + return 0; } +static int env_has_permission(const char *env_var) { + const char *val = getenv(env_var); + return val && strcmp(val, "1") == 0; +} + int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s [args...]\n", argv[0]); + fprintf(stderr, "Environment variables:\n"); + fprintf(stderr, " MARATHON_PERM_NETWORK=1 - Allow unrestricted network access\n"); + fprintf(stderr, " MARATHON_PERM_STORAGE=1 - Allow full home directory write access\n"); return 1; } openlog("marathon-sandbox", LOG_PID | LOG_NDELAY, LOG_AUTH); - if (setup_landlock() < 0) { - syslog(LOG_WARNING, "[SECURITY] Landlock setup failed"); - fprintf(stderr, "[marathon-sandbox] Warning: continuing without Landlock\n"); + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + perror("[marathon-sandbox] Failed to set no_new_privs"); + return 1; + } + + int has_network = env_has_permission("MARATHON_PERM_NETWORK"); + int has_storage = env_has_permission("MARATHON_PERM_STORAGE"); + + if (setup_landlock(has_network, has_storage) < 0) { + syslog(LOG_ERR, "[SECURITY] Landlock setup failed - Aborting"); + fprintf(stderr, "[marathon-sandbox] Error: Landlock setup failed. Aborting.\n"); + return 1; } if (setup_seccomp() < 0) { - syslog(LOG_WARNING, "[SECURITY] Seccomp setup failed"); - fprintf(stderr, "[marathon-sandbox] Warning: continuing without seccomp\n"); + syslog(LOG_ERR, "[SECURITY] Seccomp setup failed - Aborting"); + fprintf(stderr, "[marathon-sandbox] Error: Seccomp setup failed. Aborting.\n"); + return 1; } closelog();