From 7780cd0cc1fe349b54ecff3f6d79b292471b4fea Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Wed, 25 Jun 2025 12:40:00 +0000 Subject: [PATCH] Implement usr_drift_protected_paths, mark transient in history Resolves: RHEL-84515 Resolves: RHEL-84501 --- ...tore-persist-transient-in-history-DB.patch | 125 ++++++++++++++++++ ...persist-or-transient-in-history-info.patch | 31 +++++ ...ry-persistence-for-MergedTransaction.patch | 58 ++++++++ ...her-protected-paths-will-be-modified.patch | 52 ++++++++ ...-etc-dnf-usr_drift_protected_paths.d.patch | 57 ++++++++ ...t-globs-in-usr_drift_protected_paths.patch | 35 +++++ ...c-Document-usr_drift_protected_paths.patch | 32 +++++ ...f-there-are-any-usr_drift_protected_.patch | 29 ++++ dnf.spec | 19 ++- 9 files changed, 435 insertions(+), 3 deletions(-) create mode 100644 0025-persistence-store-persist-transient-in-history-DB.patch create mode 100644 0026-Print-persist-or-transient-in-history-info.patch create mode 100644 0027-history-persistence-for-MergedTransaction.patch create mode 100644 0028-bootc-Check-whether-protected-paths-will-be-modified.patch create mode 100644 0029-spec-package-etc-dnf-usr_drift_protected_paths.d.patch create mode 100644 0030-Support-globs-in-usr_drift_protected_paths.patch create mode 100644 0031-doc-Document-usr_drift_protected_paths.patch create mode 100644 0032-Load-filelists-if-there-are-any-usr_drift_protected_.patch diff --git a/0025-persistence-store-persist-transient-in-history-DB.patch b/0025-persistence-store-persist-transient-in-history-DB.patch new file mode 100644 index 0000000..c7d4fe0 --- /dev/null +++ b/0025-persistence-store-persist-transient-in-history-DB.patch @@ -0,0 +1,125 @@ +From 174079ad0ebbb1b76b98c12af4291b71bd893eed Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Thu, 15 May 2025 20:48:58 +0000 +Subject: [PATCH 1/8] persistence: store persist/transient in history DB + +For all transactions, store whether the transaction was persistent or +transient in the history DB. Requires libdnf 0.75.0. + +Moves some of the bootc logic to the `configure` phase. + +For https://github.com/rpm-software-management/dnf/issues/2196. +--- + dnf.spec | 2 +- + dnf/base.py | 6 ++++-- + dnf/cli/cli.py | 4 ++++ + dnf/db/history.py | 8 +++++++- + 4 files changed, 16 insertions(+), 4 deletions(-) + +diff --git a/dnf.spec b/dnf.spec +index b843f4e6e..c9aacfca2 100644 +--- a/dnf.spec ++++ b/dnf.spec +@@ -2,7 +2,7 @@ + %define __cmake_in_source_build 1 + + # default dependencies +-%global hawkey_version 0.74.0 ++%global hawkey_version 0.75.0 + %global libcomps_version 0.1.8 + %global libmodulemd_version 2.9.3 + %global rpm_version 4.14.0 +diff --git a/dnf/base.py b/dnf/base.py +index c4a4f8543..7117dbe34 100644 +--- a/dnf/base.py ++++ b/dnf/base.py +@@ -118,6 +118,7 @@ class Base(object): + self._update_security_options = {} + self._allow_erasing = False + self._repo_set_imported_gpg_keys = set() ++ self._persistence = libdnf.transaction.TransactionPersistence_UNKNOWN + self.output = None + + def __enter__(self): +@@ -973,7 +974,7 @@ class Base(object): + else: + rpmdb_version = old.end_rpmdb_version + +- self.history.beg(rpmdb_version, [], [], cmdline) ++ self.history.beg(rpmdb_version, [], [], cmdline=cmdline, persistence=self._persistence) + self.history.end(rpmdb_version) + self._plugins.run_pre_transaction() + self._plugins.run_transaction() +@@ -1124,7 +1125,8 @@ class Base(object): + cmdline = ' '.join(self.cmds) + + comment = self.conf.comment if self.conf.comment else "" +- tid = self.history.beg(rpmdbv, using_pkgs, [], cmdline, comment) ++ tid = self.history.beg(rpmdbv, using_pkgs, [], cmdline=cmdline, ++ comment=comment, persistence=self._persistence) + + if self.conf.reset_nice: + onice = os.nice(0) +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 0be9559d4..f2a1a99ee 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -244,11 +244,14 @@ class BaseCli(dnf.Base): + logger.info(_("A transient overlay will be created on /usr that will be discarded on reboot. " + "Keep in mind that changes to /etc and /var will still persist, and packages " + "commonly modify these directories.")) ++ self._persistence = libdnf.transaction.TransactionPersistence_TRANSIENT + else: + # Not a bootc transaction. + if self.conf.persistence == "transient": + raise CliError(_("Transient transactions are only supported on bootc systems.")) + ++ self._persistence = libdnf.transaction.TransactionPersistence_PERSIST ++ + if self._promptWanted(): + if self.conf.assumeno or not self.output.userconfirm(): + raise CliError(_("Operation aborted.")) +@@ -945,6 +948,7 @@ class Cli(object): + ) + ) + ++ + def _read_conf_file(self, releasever=None, releasever_major=None, releasever_minor=None): + timer = dnf.logging.Timer('config') + conf = self.base.conf +diff --git a/dnf/db/history.py b/dnf/db/history.py +index bf9020ad0..2cde9cbc8 100644 +--- a/dnf/db/history.py ++++ b/dnf/db/history.py +@@ -222,6 +222,10 @@ class TransactionWrapper(object): + def comment(self): + return self._trans.getComment() + ++ @property ++ def persistence(self): ++ return self._trans.getPersistence() ++ + def tids(self): + return [self._trans.getId()] + +@@ -418,7 +422,8 @@ class SwdbInterface(object): + # return result + + # TODO: rename to begin_transaction? +- def beg(self, rpmdb_version, using_pkgs, tsis, cmdline=None, comment=""): ++ def beg(self, rpmdb_version, using_pkgs, tsis, cmdline=None, comment="", ++ persistence=libdnf.transaction.TransactionPersistence_UNKNOWN): + try: + self.swdb.initTransaction() + except: +@@ -431,6 +436,7 @@ class SwdbInterface(object): + int(misc.getloginuid()), + comment) + self.swdb.setReleasever(self.releasever) ++ self.swdb.setPersistence(persistence) + self._tid = tid + + return tid +-- +2.49.0 + diff --git a/0026-Print-persist-or-transient-in-history-info.patch b/0026-Print-persist-or-transient-in-history-info.patch new file mode 100644 index 0000000..236a195 --- /dev/null +++ b/0026-Print-persist-or-transient-in-history-info.patch @@ -0,0 +1,31 @@ +From e05c700cf386c7e38762be6542967ca494adfce9 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 16 May 2025 20:38:56 +0000 +Subject: [PATCH 2/8] Print "persist" or "transient" in history info + +--- + dnf/cli/output.py | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/dnf/cli/output.py b/dnf/cli/output.py +index 1e12c9b48..4699f7800 100644 +--- a/dnf/cli/output.py ++++ b/dnf/cli/output.py +@@ -1777,6 +1777,14 @@ Transaction Summary + else: + print(_("Command Line :"), old.cmdline) + ++ if old.persistence == libdnf.transaction.TransactionPersistence_PERSIST: ++ persistence_str = "Persist" ++ elif old.persistence == libdnf.transaction.TransactionPersistence_TRANSIENT: ++ persistence_str = "Transient" ++ else: ++ persistence_str = "Unknown" ++ print(_("Persistence :"), persistence_str) ++ + if old.comment is not None: + if isinstance(old.comment, (list, tuple)): + for comment in old.comment: +-- +2.49.0 + diff --git a/0027-history-persistence-for-MergedTransaction.patch b/0027-history-persistence-for-MergedTransaction.patch new file mode 100644 index 0000000..bc79bf4 --- /dev/null +++ b/0027-history-persistence-for-MergedTransaction.patch @@ -0,0 +1,58 @@ +From cfe76d5dfacee662cf54ab9d132df5ac2e0be4b6 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 19 May 2025 22:36:50 +0000 +Subject: [PATCH 3/8] history: persistence for MergedTransaction + +--- + dnf/cli/output.py | 18 ++++++++++++------ + dnf/db/history.py | 4 ++++ + 2 files changed, 16 insertions(+), 6 deletions(-) + +diff --git a/dnf/cli/output.py b/dnf/cli/output.py +index 4699f7800..5238b356e 100644 +--- a/dnf/cli/output.py ++++ b/dnf/cli/output.py +@@ -1777,13 +1777,19 @@ Transaction Summary + else: + print(_("Command Line :"), old.cmdline) + +- if old.persistence == libdnf.transaction.TransactionPersistence_PERSIST: +- persistence_str = "Persist" +- elif old.persistence == libdnf.transaction.TransactionPersistence_TRANSIENT: +- persistence_str = "Transient" ++ def print_persistence(persistence): ++ if old.persistence == libdnf.transaction.TransactionPersistence_PERSIST: ++ persistence_str = "Persist" ++ elif old.persistence == libdnf.transaction.TransactionPersistence_TRANSIENT: ++ persistence_str = "Transient" ++ else: ++ persistence_str = "Unknown" ++ print(_("Persistence :"), persistence_str) ++ if isinstance(old.persistence, (list, tuple)): ++ for persistence in old.persistence: ++ print_persistence(persistence) + else: +- persistence_str = "Unknown" +- print(_("Persistence :"), persistence_str) ++ print_persistence(old.persistence) + + if old.comment is not None: + if isinstance(old.comment, (list, tuple)): +diff --git a/dnf/db/history.py b/dnf/db/history.py +index 2cde9cbc8..a2c1a8882 100644 +--- a/dnf/db/history.py ++++ b/dnf/db/history.py +@@ -269,6 +269,10 @@ class MergedTransactionWrapper(TransactionWrapper): + def cmdline(self): + return self._trans.listCmdlines() + ++ @property ++ def persistence(self): ++ return self._trans.listPersistences() ++ + @property + def releasever(self): + return self._trans.listReleasevers() +-- +2.49.0 + diff --git a/0028-bootc-Check-whether-protected-paths-will-be-modified.patch b/0028-bootc-Check-whether-protected-paths-will-be-modified.patch new file mode 100644 index 0000000..70ec6a1 --- /dev/null +++ b/0028-bootc-Check-whether-protected-paths-will-be-modified.patch @@ -0,0 +1,52 @@ +From e651b6e6ccaac103bbaa1440030cc13e736efe63 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Wed, 28 May 2025 20:46:56 +0000 +Subject: [PATCH 4/8] bootc: Check whether protected paths will be modified + +For https://github.com/rpm-software-management/dnf/issues/2199. + +Requires libdnf 0.75.0 with the new `usr_drift_protected_paths` option. +--- + dnf/cli/cli.py | 19 +++++++++++++++++++ + 1 file changed, 19 insertions(+) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index f2a1a99ee..b6600e99d 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -29,6 +29,7 @@ try: + from collections.abc import Sequence + except ImportError: + from collections import Sequence ++from collections import defaultdict + import datetime + import logging + import operator +@@ -245,6 +246,24 @@ class BaseCli(dnf.Base): + "Keep in mind that changes to /etc and /var will still persist, and packages " + "commonly modify these directories.")) + self._persistence = libdnf.transaction.TransactionPersistence_TRANSIENT ++ ++ # Check whether the transaction modifies usr_drift_protected_paths ++ transaction_protected_paths = defaultdict(list) ++ for pkg in trans: ++ for pkg_file_path in sorted(pkg.files): ++ for protected_path in self.conf.usr_drift_protected_paths: ++ if pkg_file_path.startswith("%s/" % protected_path) or pkg_file_path == protected_path: ++ transaction_protected_paths[pkg.nevra].append(pkg_file_path) ++ if transaction_protected_paths: ++ logger.info(_('This operation would modify the following paths, possibly introducing ' ++ 'inconsistencies when the transient overlay on /usr is discarded. See the ' ++ 'usr_drift_protected_paths configuration option for more information.')) ++ for nevra, protected_paths in transaction_protected_paths.items(): ++ logger.info(nevra) ++ for protected_path in protected_paths: ++ logger.info(" %s" % protected_path) ++ raise CliError(_("Operation aborted.")) ++ + else: + # Not a bootc transaction. + if self.conf.persistence == "transient": +-- +2.49.0 + diff --git a/0029-spec-package-etc-dnf-usr_drift_protected_paths.d.patch b/0029-spec-package-etc-dnf-usr_drift_protected_paths.d.patch new file mode 100644 index 0000000..2f362d8 --- /dev/null +++ b/0029-spec-package-etc-dnf-usr_drift_protected_paths.d.patch @@ -0,0 +1,57 @@ +From 8402e368cceb3bbe6b921b3731a75e4194842f46 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 3 Jun 2025 22:43:46 +0000 +Subject: [PATCH 5/8] spec: package /etc/dnf/usr_drift_protected_paths.d + +--- + dnf.spec | 1 + + dnf/cli/cli.py | 2 +- + etc/dnf/CMakeLists.txt | 1 + + etc/dnf/usr-drift-protected-paths.d/CMakeLists.txt | 1 + + 4 files changed, 4 insertions(+), 1 deletion(-) + create mode 100644 etc/dnf/usr-drift-protected-paths.d/CMakeLists.txt + +diff --git a/dnf.spec b/dnf.spec +index c9aacfca2..1e7dde917 100644 +--- a/dnf.spec ++++ b/dnf.spec +@@ -332,6 +332,7 @@ popd + %dir %{pluginconfpath} + %if %{without dnf5_obsoletes_dnf} + %dir %{confdir}/protected.d ++%dir %{confdir}/usr-drift-protected-paths.d + %dir %{confdir}/vars + %endif + %dir %{confdir}/aliases.d +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index b6600e99d..8816cfc3f 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -262,7 +262,7 @@ class BaseCli(dnf.Base): + logger.info(nevra) + for protected_path in protected_paths: + logger.info(" %s" % protected_path) +- raise CliError(_("Operation aborted.")) ++ raise CliError(_("Operation aborted. Pass --setopt=usr_drift_protected_paths= to disable this check and proceed anyway.")) + + else: + # Not a bootc transaction. +diff --git a/etc/dnf/CMakeLists.txt b/etc/dnf/CMakeLists.txt +index 6f0eec94e..7a60186d7 100644 +--- a/etc/dnf/CMakeLists.txt ++++ b/etc/dnf/CMakeLists.txt +@@ -1,3 +1,4 @@ + INSTALL (FILES "dnf-strict.conf" "dnf.conf" "automatic.conf" DESTINATION ${SYSCONFDIR}/dnf) + ADD_SUBDIRECTORY (aliases.d) + ADD_SUBDIRECTORY (protected.d) ++ADD_SUBDIRECTORY (usr-drift-protected-paths.d) +diff --git a/etc/dnf/usr-drift-protected-paths.d/CMakeLists.txt b/etc/dnf/usr-drift-protected-paths.d/CMakeLists.txt +new file mode 100644 +index 000000000..206b1b281 +--- /dev/null ++++ b/etc/dnf/usr-drift-protected-paths.d/CMakeLists.txt +@@ -0,0 +1 @@ ++INSTALL(DIRECTORY DESTINATION ${SYSCONFDIR}/dnf/usr-drift-protected-paths.d) +-- +2.49.0 + diff --git a/0030-Support-globs-in-usr_drift_protected_paths.patch b/0030-Support-globs-in-usr_drift_protected_paths.patch new file mode 100644 index 0000000..264a40e --- /dev/null +++ b/0030-Support-globs-in-usr_drift_protected_paths.patch @@ -0,0 +1,35 @@ +From d79d2432dae005d61797a3715760a7f671d4eba8 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 6 Jun 2025 21:24:20 +0000 +Subject: [PATCH 6/8] Support globs in usr_drift_protected_paths + +--- + dnf/cli/cli.py | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 8816cfc3f..289c88224 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -31,6 +31,7 @@ except ImportError: + from collections import Sequence + from collections import defaultdict + import datetime ++from fnmatch import fnmatch + import logging + import operator + import os +@@ -251,8 +252,8 @@ class BaseCli(dnf.Base): + transaction_protected_paths = defaultdict(list) + for pkg in trans: + for pkg_file_path in sorted(pkg.files): +- for protected_path in self.conf.usr_drift_protected_paths: +- if pkg_file_path.startswith("%s/" % protected_path) or pkg_file_path == protected_path: ++ for protected_pattern in self.conf.usr_drift_protected_paths: ++ if fnmatch(pkg_file_path, protected_pattern): + transaction_protected_paths[pkg.nevra].append(pkg_file_path) + if transaction_protected_paths: + logger.info(_('This operation would modify the following paths, possibly introducing ' +-- +2.49.0 + diff --git a/0031-doc-Document-usr_drift_protected_paths.patch b/0031-doc-Document-usr_drift_protected_paths.patch new file mode 100644 index 0000000..0358ceb --- /dev/null +++ b/0031-doc-Document-usr_drift_protected_paths.patch @@ -0,0 +1,32 @@ +From 372075e7a3df6064a3a7680f42284887d6a2a763 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 6 Jun 2025 22:31:27 +0000 +Subject: [PATCH 7/8] doc: Document `usr_drift_protected_paths` + +--- + doc/conf_ref.rst | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/doc/conf_ref.rst b/doc/conf_ref.rst +index 441aa77b3..8a0836de5 100644 +--- a/doc/conf_ref.rst ++++ b/doc/conf_ref.rst +@@ -567,6 +567,15 @@ configuration file by your distribution to override the DNF defaults. + + Set this to False to disable the automatic running of ``group upgrade`` when running the ``upgrade`` command. Default is ``True`` (perform the operation). + ++.. _usr_drift_protected_paths-label: ++ ++``usr_drift_protected_paths`` ++ :ref:`list ` ++ ++ List of paths that are likely to cause problems when their contents drift with respect to ``/usr``, e.g. ``/etc/pam.d/*``. If a transient transaction would modify these paths, DNF aborts the operation and prints an error. Supports globs. Defaults to ``glob:/etc/dnf/usr-drift-protected-paths.d/*.conf``. So a list of paths can be protected by creating a ``.conf`` file in ``/etc/dnf/usr-drift-protected-paths.d/`` containing one path (or glob pattern) per line. ++ ++ When using ``persistence=transient`` on bootc systems, a transient overlay is created on ``/usr``, and any changes DNF makes to ``/usr`` will be discarded on reboot. However, other paths such as ``/etc`` and ``/var`` are (often) not backed by a transient overlay, so changes to them will persist across reboots. Usually, this "filesystem drift" is fine, but it can cause problems in certain situations. For example, a configuration file in ``/etc`` that's shared by multiple packages might reference a ``.so`` file under ``/usr/lib64`` that no longer exists. ++ + .. _varsdir_options-label: + + ``varsdir`` +-- +2.49.0 + diff --git a/0032-Load-filelists-if-there-are-any-usr_drift_protected_.patch b/0032-Load-filelists-if-there-are-any-usr_drift_protected_.patch new file mode 100644 index 0000000..4c6e6aa --- /dev/null +++ b/0032-Load-filelists-if-there-are-any-usr_drift_protected_.patch @@ -0,0 +1,29 @@ +From 8b6b08aeb3c162a774f1a63f85ea62969c0d375c Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 17 Jun 2025 18:52:53 +0000 +Subject: [PATCH 8/8] Load filelists if there are any usr_drift_protected_paths + +--- + dnf/conf/config.py | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/dnf/conf/config.py b/dnf/conf/config.py +index 7e5e8a38d..145016f6d 100644 +--- a/dnf/conf/config.py ++++ b/dnf/conf/config.py +@@ -417,6 +417,12 @@ class MainConf(BaseConfig): + if skip_broken_val: + self._set_value('strict', not skip_broken_val, self._get_priority('skip_broken')) + ++ usr_drift_protected_paths = self._get_value('usr_drift_protected_paths') ++ if usr_drift_protected_paths: ++ optional_metadata_types = set(self._get_value('optional_metadata_types')) ++ optional_metadata_types.add('filelists') ++ self._set_value('optional_metadata_types', list(optional_metadata_types)) ++ + @property + def releasever(self): + # :api +-- +2.49.0 + diff --git a/dnf.spec b/dnf.spec index 87ca54a..9b80a67 100644 --- a/dnf.spec +++ b/dnf.spec @@ -2,7 +2,7 @@ %define __cmake_in_source_build 1 # default dependencies -%global hawkey_version 0.74.0 +%global hawkey_version 0.75.0 %global libcomps_version 0.1.8 %global libmodulemd_version 2.9.3 %global rpm_version 4.14.0 @@ -24,7 +24,7 @@ %endif %if 0%{?rhel} == 10 - %global hawkey_version 0.73.1-8 + %global hawkey_version 0.73.1-11 %endif # override dependencies for fedora 26 @@ -72,7 +72,7 @@ It supports RPMs, modules and comps groups & environments. Name: dnf Version: 4.20.0 -Release: 14%{?dist} +Release: 15%{?dist} Summary: %{pkg_summary} # For a breakdown of the licensing, see PACKAGE-LICENSING License: GPL-2.0-or-later AND GPL-1.0-only @@ -102,6 +102,14 @@ Patch21: 0021-doc-Document-detect_releasevers-and-update-example.patch Patch22: 0022-tests-Patch-detect_releasevers-not-detect_releasever.patch Patch23: 0023-Document-how-releasever-releasever_-major-minor-affe.patch Patch24: 0024-package-remote_location-takes-basedir-into-account.patch +Patch25: 0025-persistence-store-persist-transient-in-history-DB.patch +Patch26: 0026-Print-persist-or-transient-in-history-info.patch +Patch27: 0027-history-persistence-for-MergedTransaction.patch +Patch28: 0028-bootc-Check-whether-protected-paths-will-be-modified.patch +Patch29: 0029-spec-package-etc-dnf-usr_drift_protected_paths.d.patch +Patch30: 0030-Support-globs-in-usr_drift_protected_paths.patch +Patch31: 0031-doc-Document-usr_drift_protected_paths.patch +Patch32: 0032-Load-filelists-if-there-are-any-usr_drift_protected_.patch BuildArch: noarch BuildRequires: cmake @@ -357,6 +365,7 @@ popd %dir %{pluginconfpath} %if %{without dnf5_obsoletes_dnf} %dir %{confdir}/protected.d +%dir %{confdir}/usr-drift-protected-paths.d %dir %{confdir}/vars %endif %dir %{confdir}/aliases.d @@ -458,6 +467,10 @@ popd # bootc subpackage does not include any files %changelog +* Wed Jun 25 2025 Evan Goode - 4.20.0-15 +- Mark transient transactions in DNF history (RHEL-84515) +- Warn/disallow changes outside /usr, /etc with --transient (RHEL-84501) + * Fri Apr 04 2025 Evan Goode - 4.20.0-14 - spec: Provide dnf4 by python3-dnf (RHEL-82311)