From 61ef199667071f1dcb806d4ce57f0e0479c603c4 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Sat, 12 Sep 2026 05:51:06 +0000 Subject: [PATCH] Fix update discovery and isolate fingerprint installer permissions --- fedora-tools-settings/CMakeLists.txt | 2 + fedora-tools-settings/README.md | 5 +++ .../fedora-tools-settings.spec | 7 +++- fedora-tools-settings/src/CMakeLists.txt | 1 + fedora-tools-settings/src/fedoratoolskcm.cpp | 42 ++++--------------- fedora-tools-settings/src/fedoratoolskcm.h | 3 -- fedora-tools-settings/src/toolmodel.cpp | 20 +++++++-- fedora-tools-settings/src/toolmodel.h | 4 +- fedora-tools-settings/tests/CMakeLists.txt | 2 +- .../tests/test-toolmodel.cpp | 36 +++++++++++++++- fedora-tools.repo | 1 + .../plasma-fingerprint-workaround | 10 ++++- .../plasma-fingerprint-workaround.spec | 5 ++- .../tests/test-controller | 19 +++++++++ 14 files changed, 106 insertions(+), 51 deletions(-) diff --git a/fedora-tools-settings/CMakeLists.txt b/fedora-tools-settings/CMakeLists.txt index aa30aed..3104db9 100644 --- a/fedora-tools-settings/CMakeLists.txt +++ b/fedora-tools-settings/CMakeLists.txt @@ -17,6 +17,8 @@ include(KDECompilerSettings NO_POLICY_SCOPE) find_package(Qt6 6.8 REQUIRED COMPONENTS Core Quick) find_package(KF6 6.0 REQUIRED COMPONENTS Auth Config CoreAddons I18n KCMUtils) +find_package(PkgConfig REQUIRED) +pkg_check_modules(RPM REQUIRED IMPORTED_TARGET rpm) add_subdirectory(src) if(BUILD_TESTING) diff --git a/fedora-tools-settings/README.md b/fedora-tools-settings/README.md index 319bedf..fbaa419 100644 --- a/fedora-tools-settings/README.md +++ b/fedora-tools-settings/README.md @@ -14,6 +14,11 @@ installation to a narrowly scoped KAuth helper. The helper accepts only valid package names that independently resolve to that capability in the `fedora-tools` repository. +Update detection compares repository and installed versions using RPM's version +ordering, without requiring access to DNF's privileged transaction state. DNF +still checks dependencies when installing an update. The repository file sets a +five-minute metadata expiry so Discover's PackageKit backend checks it regularly. + Installed tools can also be configured or removed from the module. Package operations run one at a time; other actions remain disabled until DNF finishes. The module currently configures task-group shortcut behaviour, touchpad hold-tap diff --git a/fedora-tools-settings/fedora-tools-settings.spec b/fedora-tools-settings/fedora-tools-settings.spec index bf7f514..beea4cb 100644 --- a/fedora-tools-settings/fedora-tools-settings.spec +++ b/fedora-tools-settings/fedora-tools-settings.spec @@ -1,6 +1,6 @@ Name: fedora-tools-settings Version: 0.1.0 -Release: 13%{?dist} +Release: 14%{?dist} Summary: Plasma System Settings module for Fedora Tools License: MIT @@ -18,6 +18,7 @@ BuildRequires: kf6-kconfig-devel BuildRequires: kf6-ki18n-devel BuildRequires: kf6-kirigami-devel BuildRequires: qt6-qtdeclarative-devel +BuildRequires: rpm-devel Requires: dnf5 Requires: kf6-kauth @@ -74,6 +75,10 @@ done %{_datadir}/polkit-1/actions/se.ajpanton.fedoratools.policy %changelog +* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-14 +- Detect newer packages without reading privileged DNF state +- Report package query failures instead of silently hiding updates + * Sat Sep 12 2026 fedora-tools contributors - 0.1.0-13 - Share settings validation and report failed configuration writes - Simplify ordering and source packaging diff --git a/fedora-tools-settings/src/CMakeLists.txt b/fedora-tools-settings/src/CMakeLists.txt index cd4d1d3..2d84ed1 100644 --- a/fedora-tools-settings/src/CMakeLists.txt +++ b/fedora-tools-settings/src/CMakeLists.txt @@ -10,6 +10,7 @@ kcmutils_add_qml_kcm(kcm_fedora_tools toolmodel.h ) target_link_libraries(kcm_fedora_tools PRIVATE + PkgConfig::RPM Qt6::Core Qt6::Quick KF6::AuthCore diff --git a/fedora-tools-settings/src/fedoratoolskcm.cpp b/fedora-tools-settings/src/fedoratoolskcm.cpp index dbc242b..bab8ff7 100644 --- a/fedora-tools-settings/src/fedoratoolskcm.cpp +++ b/fedora-tools-settings/src/fedoratoolskcm.cpp @@ -142,7 +142,6 @@ void FedoraToolsKcm::refresh(bool refreshMetadata) m_refreshMetadata = refreshMetadata; m_installedPackages.clear(); m_availablePackages.clear(); - m_updates.clear(); setMessage({}); setBusy(true); @@ -316,8 +315,12 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus const QByteArray output = m_query.readAllStandardOutput(); const QByteArray errorOutput = m_query.readAllStandardError(); - if (exitStatus != QProcess::NormalExit) { - setMessage(i18n("Package discovery terminated unexpectedly."), true); + // rpm -q reports missing packages on stdout with exit code 1. That is a + // normal discovery result; stderr or a failed repository query is not. + const QString details = QString::fromUtf8(errorOutput).trimmed(); + if (exitStatus != QProcess::NormalExit + || (exitCode != 0 && (m_stage == QueryStage::Available || !details.isEmpty() || exitCode != 1))) { + setMessage(details.isEmpty() ? i18n("Package discovery failed (%1).", m_query.program()) : details, true); m_stage = QueryStage::Idle; setPackageOperation({}, {}); setBusy(false); @@ -342,43 +345,12 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus } if (m_stage == QueryStage::Available) { - if (exitCode != 0) { - m_tools.setPackages(m_installedPackages, {}); - const QString details = QString::fromUtf8(errorOutput).trimmed(); - setMessage(details.isEmpty() ? i18n("The Fedora Tools repository is unavailable.") : details, true); - updateFingerprintStatus(); - m_stage = QueryStage::Idle; - setPackageOperation({}, {}); - setBusy(false); - return; - } - m_availablePackages = parsePackageRecords(output); if (m_availablePackages.isEmpty()) { finishRefresh(); return; } - startQuery(QStringLiteral("/usr/bin/dnf5"), - {QStringLiteral("--repo=fedora-tools"), - QStringLiteral("-q"), - QStringLiteral("repoquery"), - QStringLiteral("--available"), - QStringLiteral("--upgrades"), - QStringLiteral("--whatprovides=fedora-tools-tool"), - QStringLiteral("--queryformat"), - packageFormat}, - QueryStage::Updates); - return; - } - - if (m_stage == QueryStage::Updates) { - if (exitCode == 0) { - for (const PackageRecord &package : parsePackageRecords(output)) { - m_updates.insert(package.name); - } - } - QStringList packageNames; for (const PackageRecord &package : std::as_const(m_availablePackages)) { packageNames.append(package.name); @@ -407,7 +379,7 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus void FedoraToolsKcm::finishRefresh() { - m_tools.setPackages(m_installedPackages, m_availablePackages, m_updates); + m_tools.setPackages(m_installedPackages, m_availablePackages); updateFingerprintStatus(); m_stage = QueryStage::Idle; setPackageOperation({}, {}); diff --git a/fedora-tools-settings/src/fedoratoolskcm.h b/fedora-tools-settings/src/fedoratoolskcm.h index 9c2a92c..fdf4e47 100644 --- a/fedora-tools-settings/src/fedoratoolskcm.h +++ b/fedora-tools-settings/src/fedoratoolskcm.h @@ -8,7 +8,6 @@ #include #include -#include #include class KJob; @@ -75,7 +74,6 @@ private: Idle, InstalledProviders, Available, - Updates, KnownInstalled, }; @@ -95,7 +93,6 @@ private: QueryStage m_stage = QueryStage::Idle; QList m_installedPackages; QList m_availablePackages; - QSet m_updates; bool m_refreshMetadata = false; bool m_busy = false; bool m_error = false; diff --git a/fedora-tools-settings/src/toolmodel.cpp b/fedora-tools-settings/src/toolmodel.cpp index 2a0714b..a04ca7f 100644 --- a/fedora-tools-settings/src/toolmodel.cpp +++ b/fedora-tools-settings/src/toolmodel.cpp @@ -10,12 +10,26 @@ #include #include #include +#include #include #include namespace { +bool newerVersion(const QString &available, const QString &installed) +{ + const auto candidate = rpmverParse(available.toUtf8().constData()); + const auto current = rpmverParse(installed.toUtf8().constData()); + if (!candidate || !current) { + qWarning() << "Cannot compare RPM versions:" << available << installed; + } + const bool newer = candidate && current && rpmverCmp(candidate, current) > 0; + rpmverFree(candidate); + rpmverFree(current); + return newer; +} + QStringList readConfigurationCommand(const QString &packageName) { const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, @@ -119,8 +133,7 @@ QHash ToolModel::roleNames() const } void ToolModel::setPackages(const QList &installed, - const QList &available, - const QSet &updates) + const QList &available) { QMap tools; @@ -147,7 +160,8 @@ void ToolModel::setPackages(const QList &installed, } tool.compatibilityError = incompatible; tool.available = incompatible.isEmpty(); - tool.updateAvailable = tool.available && tool.installed && updates.contains(package.name); + tool.updateAvailable = tool.available && tool.installed + && newerVersion(package.version, tool.version); } beginResetModel(); diff --git a/fedora-tools-settings/src/toolmodel.h b/fedora-tools-settings/src/toolmodel.h index 9cfc523..b7e55fc 100644 --- a/fedora-tools-settings/src/toolmodel.h +++ b/fedora-tools-settings/src/toolmodel.h @@ -5,7 +5,6 @@ #include "packageutils.h" #include -#include #include struct Tool @@ -48,8 +47,7 @@ public: QHash roleNames() const override; void setPackages(const QList &installed, - const QList &available, - const QSet &updates = {}); + const QList &available); bool mayInstall(const QString &packageName) const; bool mayRemove(const QString &packageName) const; QStringList configurationCommand(const QString &packageName) const; diff --git a/fedora-tools-settings/tests/CMakeLists.txt b/fedora-tools-settings/tests/CMakeLists.txt index 16ac365..8d5377d 100644 --- a/fedora-tools-settings/tests/CMakeLists.txt +++ b/fedora-tools-settings/tests/CMakeLists.txt @@ -14,7 +14,7 @@ add_executable(test-toolmodel ../src/packageutils.cpp ) target_include_directories(test-toolmodel PRIVATE ../src) -target_link_libraries(test-toolmodel PRIVATE Qt6::Core Qt6::Test) +target_link_libraries(test-toolmodel PRIVATE Qt6::Core Qt6::Test PkgConfig::RPM) add_test(NAME toolmodel COMMAND test-toolmodel) add_executable(test-qmlresource test-qmlresource.cpp) diff --git a/fedora-tools-settings/tests/test-toolmodel.cpp b/fedora-tools-settings/tests/test-toolmodel.cpp index 6a927da..4973c29 100644 --- a/fedora-tools-settings/tests/test-toolmodel.cpp +++ b/fedora-tools-settings/tests/test-toolmodel.cpp @@ -12,6 +12,38 @@ class ToolModelTest : public QObject Q_OBJECT private Q_SLOTS: + void comparesRpmVersions_data() + { + QTest::addColumn("installed"); + QTest::addColumn("available"); + QTest::addColumn("update"); + QTest::newRow("numeric release") << QStringLiteral("0.1.0-9.fc44") << QStringLiteral("0.1.0-11.fc44") << true; + QTest::newRow("same version") << QStringLiteral("0.1.0-11.fc44") << QStringLiteral("0.1.0-11.fc44") << false; + QTest::newRow("locally newer") << QStringLiteral("0.1.0-25.fc44") << QStringLiteral("0.1.0-23.fc44") << false; + QTest::newRow("epoch wins") << QStringLiteral("2.0-1") << QStringLiteral("1:1.0-1") << true; + QTest::newRow("installed epoch wins") << QStringLiteral("1:1.0-1") << QStringLiteral("2.0-1") << false; + QTest::newRow("implicit zero epoch") << QStringLiteral("0:1.0-1") << QStringLiteral("1.0-1") << false; + QTest::newRow("prerelease") << QStringLiteral("1.0~rc1-1") << QStringLiteral("1.0-1") << true; + QTest::newRow("snapshot") << QStringLiteral("1.0-1") << QStringLiteral("1.0^git1-1") << true; + QTest::newRow("newer than snapshot") << QStringLiteral("1.0^git1-1") << QStringLiteral("1.0.1-1") << true; + } + + void comparesRpmVersions() + { + QFETCH(QString, installed); + QFETCH(QString, available); + QFETCH(bool, update); + ToolModel model; + // Match an older noarch install against its native-architecture update. + // No DNF transaction state or privileged files are needed to compare EVRs. + const PackageRecord current{QStringLiteral("touchpad-hold-tap"), installed, QStringLiteral("noarch"), QStringLiteral("Gesture")}; + const PackageRecord candidate{current.name, available, QStringLiteral("x86_64"), current.summary}; + model.setPackages({current}, {candidate}); + QCOMPARE(model.data(model.index(0), ToolModel::UpdateAvailableRole).toBool(), update); + model.setPackages({}, {candidate}); + QVERIFY(!model.data(model.index(0), ToolModel::UpdateAvailableRole).toBool()); + } + void incompatibleFrameworkIsOnlyListedWhenInstalled() { if (packageCompatibilityError(QStringLiteral("framework-laptop-tools")).isEmpty()) QSKIP("Requires a non-Framework test host"); const PackageRecord package{QStringLiteral("framework-laptop-tools"), QStringLiteral("1"), QStringLiteral("x86_64"), QStringLiteral("Hardware controls")}; @@ -55,7 +87,7 @@ private Q_SLOTS: const PackageRecord installed{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-5.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")}; const PackageRecord available{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-6.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")}; - model.setPackages({installed}, {available}, {QStringLiteral("touchpad-hold-tap")}); + model.setPackages({installed}, {available}); QCOMPARE(model.rowCount(), 1); const QModelIndex index = model.index(0); @@ -67,7 +99,7 @@ private Q_SLOTS: QCOMPARE(model.mayRemove(QStringLiteral("missing-tool")), false); } - void doesNotGuessUpdatesFromDifferentVersionStrings() + void doesNotOfferDowngrades() { ToolModel model; const PackageRecord installed{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-7.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")}; diff --git a/fedora-tools.repo b/fedora-tools.repo index e4c3d29..e1183fc 100644 --- a/fedora-tools.repo +++ b/fedora-tools.repo @@ -2,5 +2,6 @@ name=Small tools for Fedora baseurl=https://git.ajpanton.se/api/packages/ajp_anton/rpm/fedora/$releasever enabled=1 +metadata_expire=300 gpgcheck=1 gpgkey=https://git.ajpanton.se/api/packages/ajp_anton/rpm/repository.key diff --git a/plasma-fingerprint-workaround/plasma-fingerprint-workaround b/plasma-fingerprint-workaround/plasma-fingerprint-workaround index e841739..d51e909 100755 --- a/plasma-fingerprint-workaround/plasma-fingerprint-workaround +++ b/plasma-fingerprint-workaround/plasma-fingerprint-workaround @@ -36,6 +36,12 @@ require_fedora() { [[ ${ID:-} == fedora ]] || die "this workaround only supports Fedora Linux" } +run_dnf() ( + # The private download's umask must not make DNF's system state root-only. + umask 022 + dnf5 "$@" +) + cleanup() { [[ -n $temporary_dir ]] || return rm -f -- "$temporary_dir/kscreenlocker.rpm" @@ -233,7 +239,7 @@ enable_workaround() { dnf_args=(install --assumeyes) (( force )) && dnf_args+=(--allow-downgrade) - dnf5 "${dnf_args[@]}" "$candidate" + run_dnf "${dnf_args[@]}" "$candidate" is_workaround_package -q kscreenlocker || die "DNF completed without enabling the workaround" @@ -264,7 +270,7 @@ disable_workaround() { printf 'This will replace the workaround with Fedora\x27s current KScreenLocker package.\n' confirm - dnf5 \ + run_dnf \ --repo=fedora,updates \ --refresh \ distro-sync \ diff --git a/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec b/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec index 94b8750..9eff85e 100644 --- a/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec +++ b/plasma-fingerprint-workaround/plasma-fingerprint-workaround.spec @@ -1,6 +1,6 @@ Name: plasma-fingerprint-workaround Version: 0.1.0 -Release: 5%{?dist} +Release: 6%{?dist} Summary: Opt-in patched KScreenLocker for fingerprint recovery after suspend License: MIT @@ -51,6 +51,9 @@ install -D -m 0644 %{SOURCE3} \ %{_datadir}/plasma-fingerprint-workaround/payload.conf %changelog +* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-6 +- Keep private-download permissions from leaking into DNF's system state + * Sat Sep 12 2026 fedora-tools contributors - 0.1.0-5 - Simplify status queries and extend invalid-command tests diff --git a/plasma-fingerprint-workaround/tests/test-controller b/plasma-fingerprint-workaround/tests/test-controller index 889cba4..0364b77 100755 --- a/plasma-fingerprint-workaround/tests/test-controller +++ b/plasma-fingerprint-workaround/tests/test-controller @@ -29,4 +29,23 @@ source "$payload_config" [[ $payload_url == https://*/*.rpm ]] [[ $payload_sha256 =~ ^[[:xdigit:]]{64}$ ]] +# Load the functions through the harmless help command, then replace DNF with +# a test function. Never invoke the package manager in this test. +( + source "$controller" --help >/dev/null + dnf5() { + [[ $(umask) == 0022 && $1 == test-argument ]] + } + umask 077 + run_dnf test-argument + [[ $(umask) == 0077 ]] + dnf5() { return 23; } + if run_dnf test-argument; then + printf 'DNF failure was not propagated.\n' >&2 + exit 1 + else + [[ $? == 23 ]] + fi +) + printf 'Controller tests passed.\n'