From 9345f9e5745533cdbe28f551f72ce871effad9f6 Mon Sep 17 00:00:00 2001 From: AlmaLinux RelEng Bot Date: Mon, 24 Aug 2026 08:59:47 -0400 Subject: [PATCH] import CS dnf-4.14.0-37.el9 --- ...ootc-unlock-only-if-usr-is-read-only.patch | 57 +++ ...writable-when-DeploymentUnlockedStat.patch | 37 ++ ...0072-Preserve-ACL-when-rotating-logs.patch | 64 ++++ ...-Persistence-display-for-interval-qu.patch | 38 ++ ...-read-write-and-blocking-non-blockin.patch | 59 ++++ ...Default-to-just-_Default-allow-choic.patch | 50 +++ .../0076-Add-clean-command-lock-demand.patch | 200 +++++++++++ ...s.clean_command_lock-for-commands-th.patch | 330 ++++++++++++++++++ SPECS/dnf.spec | 22 +- 9 files changed, 856 insertions(+), 1 deletion(-) create mode 100644 SOURCES/0070-bootc-unlock-only-if-usr-is-read-only.patch create mode 100644 SOURCES/0071-bootc-Call-make_writable-when-DeploymentUnlockedStat.patch create mode 100644 SOURCES/0072-Preserve-ACL-when-rotating-logs.patch create mode 100644 SOURCES/0073-history-info-Fix-Persistence-display-for-interval-qu.patch create mode 100644 SOURCES/0074-Add-FileLock-for-read-write-and-blocking-non-blockin.patch create mode 100644 SOURCES/0075-Generalize-_BoolDefault-to-just-_Default-allow-choic.patch create mode 100644 SOURCES/0076-Add-clean-command-lock-demand.patch create mode 100644 SOURCES/0077-Configure-demands.clean_command_lock-for-commands-th.patch diff --git a/SOURCES/0070-bootc-unlock-only-if-usr-is-read-only.patch b/SOURCES/0070-bootc-unlock-only-if-usr-is-read-only.patch new file mode 100644 index 0000000..55a562f --- /dev/null +++ b/SOURCES/0070-bootc-unlock-only-if-usr-is-read-only.patch @@ -0,0 +1,57 @@ +From a8608927848b918e82d4b78bc0312cfdb58210de Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 1 Dec 2025 13:40:26 -0500 +Subject: [PATCH 1/2] bootc: unlock only if /usr is read-only + +DNF should only run `ostree admin unlock --transient` if `/usr` is +actually read-only. `/usr` may be writable via OSTree's `root.transient = +true` even if the `ostree admin status` is not transient. + +Resolves: https://redhat.atlassian.net/browse/RHEL-138512 +--- + dnf/cli/cli.py | 4 +++- + dnf/util.py | 2 +- + 2 files changed, 4 insertions(+), 2 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 21e8764d0..f855c5eec 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -225,6 +225,7 @@ class BaseCli(dnf.Base): + # Handle bootc transactions. `--transient` must be specified if + # /usr is not already writeable. + bootc_system = None ++ bootc_system_needs_unlock = False + if is_bootc_transaction: + if self.conf.persistence == "persist": + logger.info(_("Persistent transactions aren't supported on bootc systems.")) +@@ -246,6 +247,7 @@ 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.")) ++ bootc_system_needs_unlock = True + self._persistence = libdnf.transaction.TransactionPersistence_TRANSIENT + + # Check whether the transaction modifies usr_drift_protected_paths +@@ -276,7 +278,7 @@ class BaseCli(dnf.Base): + if self.conf.assumeno or not self.output.userconfirm(): + raise CliError(_("Operation aborted.")) + +- if bootc_system: ++ if bootc_system and bootc_system_needs_unlock: + bootc_system.make_writable() + else: + logger.info(_('Nothing to do.')) +diff --git a/dnf/util.py b/dnf/util.py +index 0161f80d8..70bae605f 100644 +--- a/dnf/util.py ++++ b/dnf/util.py +@@ -741,4 +741,4 @@ class _BootcSystem: + # read-only. Set up a mount namespace for DNF. + self._set_up_mountns() + +- assert os.access(self.usr, os.W_OK) ++ assert self.is_writable() +-- +2.53.0 + diff --git a/SOURCES/0071-bootc-Call-make_writable-when-DeploymentUnlockedStat.patch b/SOURCES/0071-bootc-Call-make_writable-when-DeploymentUnlockedStat.patch new file mode 100644 index 0000000..fd9b520 --- /dev/null +++ b/SOURCES/0071-bootc-Call-make_writable-when-DeploymentUnlockedStat.patch @@ -0,0 +1,37 @@ +From b9c0a2ab768c624ad7746a175379358479df6f55 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 20 Mar 2026 16:28:14 -0400 +Subject: [PATCH 2/2] bootc: Call make_writable when + DeploymentUnlockedState.TRANSIENT + +Fixes a bug in 1afe4328334f27b45b5c4599b6f1e8ac69d465e4. +bootc_system.make_writable should still be called even when the system +is already in DeploymentUnlockedState.TRANSIENT, since the DNF mount +namespace needs to be set up either way. +--- + dnf/cli/cli.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index f855c5eec..ce81f262b 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -235,6 +235,7 @@ class BaseCli(dnf.Base): + bootc_system = dnf.util._BootcSystem() + + if not bootc_system.is_writable(): ++ bootc_system_needs_unlock = True + if self.conf.persistence == "auto": + logger.info(_("This bootc system is configured to be read-only. Pass --transient to " + "perform this transaction in a transient overlay which will reset when " +@@ -247,7 +248,6 @@ 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.")) +- bootc_system_needs_unlock = True + self._persistence = libdnf.transaction.TransactionPersistence_TRANSIENT + + # Check whether the transaction modifies usr_drift_protected_paths +-- +2.53.0 + diff --git a/SOURCES/0072-Preserve-ACL-when-rotating-logs.patch b/SOURCES/0072-Preserve-ACL-when-rotating-logs.patch new file mode 100644 index 0000000..fca97d3 --- /dev/null +++ b/SOURCES/0072-Preserve-ACL-when-rotating-logs.patch @@ -0,0 +1,64 @@ +From 51e0b326e3d33644a0e46635055ada7f4f0006d7 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= +Date: Wed, 19 Nov 2025 16:05:25 +0100 +Subject: [PATCH] Preserve ACL when rotating logs + +Upstream commit: 4324b297da2fd6a15670241398a31e7f462e44e4 + +When DNF rotated /var/log/dnf.log, it preserved a file mode, but it +lost an access control list: + + # getfacl -c /var/log/dnf.log + getfacl: Removing leading '/' from absolute path names + user::rw- + user:root:r-- + group::r-- + mask::r-- + other::r-- + + # dnf4 --setopt log_rotate=4 --setopt log_size=1 upgrade --assumeno + + # getfacl -c /var/log/dnf.log + getfacl: Removing leading '/' from absolute path names + user::rw- + group::r-- + other::r-- + +This patch fixes it by copying an extended attribute which stores the access +control list. (Python does not have an interface for handling the access +control lists.) + +Resolve: #2279 +Resolve: https://redhat.atlassian.net/browse/RHEL-122013 +--- + dnf/logging.py | 11 ++++++++++- + 1 file changed, 10 insertions(+), 1 deletion(-) + +diff --git a/dnf/logging.py b/dnf/logging.py +index ef0b25f33..7dc655932 100644 +--- a/dnf/logging.py ++++ b/dnf/logging.py +@@ -125,10 +125,19 @@ class MultiprocessRotatingFileHandler(logging.handlers.RotatingFileHandler): + try: + if self.shouldRollover(record): + with self.rotate_lock: +- # Do rollover while preserving the mode of the new log file ++ # Do rollover while preserving the mode and ACL of the new log file + mode = os.stat(self.baseFilename).st_mode ++ acl = None ++ try: ++ acl = os.getxattr(self.baseFilename, "system.posix_acl_access") ++ except: ++ # The extended attribute does not exist or the ++ # file system does not support them. ++ pass + self.doRollover() + os.chmod(self.baseFilename, mode) ++ if acl is not None: ++ os.setxattr(self.baseFilename, "system.posix_acl_access", acl) + logging.FileHandler.emit(self, record) + return + except (dnf.exceptions.ProcessLockError, dnf.exceptions.ThreadLockError): +-- +2.53.0 + diff --git a/SOURCES/0073-history-info-Fix-Persistence-display-for-interval-qu.patch b/SOURCES/0073-history-info-Fix-Persistence-display-for-interval-qu.patch new file mode 100644 index 0000000..1e5f803 --- /dev/null +++ b/SOURCES/0073-history-info-Fix-Persistence-display-for-interval-qu.patch @@ -0,0 +1,38 @@ +From 056c452cb8031766fa46c1aa6ac949018505abf6 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 9 Mar 2026 22:23:50 -0400 +Subject: [PATCH] history info: Fix Persistence display for interval queries + +print_persistence() took a persistence parameter but compared against +old.persistence (the property on the transaction object) instead. For +merged transactions, old.persistence returns a list, so comparisons +against scalar enum values always failed, displaying 'Unknown'. + +Use the function parameter instead of old.persistence. + +For: https://github.com/rpm-software-management/libdnf/issues/1744 + +Assisted-by: Claude Opus 4.6 +--- + dnf/cli/output.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/dnf/cli/output.py b/dnf/cli/output.py +index 820ab88fa..3fdd68432 100644 +--- a/dnf/cli/output.py ++++ b/dnf/cli/output.py +@@ -1773,9 +1773,9 @@ Transaction Summary + print(_("Command Line :"), old.cmdline) + + def print_persistence(persistence): +- if old.persistence == libdnf.transaction.TransactionPersistence_PERSIST: ++ if persistence == libdnf.transaction.TransactionPersistence_PERSIST: + persistence_str = "Persist" +- elif old.persistence == libdnf.transaction.TransactionPersistence_TRANSIENT: ++ elif persistence == libdnf.transaction.TransactionPersistence_TRANSIENT: + persistence_str = "Transient" + else: + persistence_str = "Unknown" +-- +2.54.0 + diff --git a/SOURCES/0074-Add-FileLock-for-read-write-and-blocking-non-blockin.patch b/SOURCES/0074-Add-FileLock-for-read-write-and-blocking-non-blockin.patch new file mode 100644 index 0000000..3c7eb26 --- /dev/null +++ b/SOURCES/0074-Add-FileLock-for-read-write-and-blocking-non-blockin.patch @@ -0,0 +1,59 @@ +From 5819856c5960f4c4d7a9acb6aaa43aca3fe68211 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Wed, 8 Jul 2026 13:16:43 +0200 +Subject: [PATCH 1/4] Add `FileLock` for read/write and blocking/non blocking + locks + +ProcessLock doesn't support read only locks and it works differently: it +effectively achieves the locking by storing a PID, flock is only used to +guard writing the PID. +--- + dnf/lock.py | 35 +++++++++++++++++++++++++++++++++++ + 1 file changed, 35 insertions(+) + +diff --git a/dnf/lock.py b/dnf/lock.py +index 6817aac9..cb189c35 100644 +--- a/dnf/lock.py ++++ b/dnf/lock.py +@@ -146,3 +146,38 @@ class ProcessLock(object): + if self.count == 1: + os.unlink(self.target) + self._unlock_thread() ++ ++ ++class FileLock(object): ++ """ ++ A file lock with read/write access and blocking/non blocking mode. ++ """ ++ ++ def __init__(self, target, description, blocking=False, read=False): ++ self.blocking = blocking ++ self.read = read ++ self.description = description ++ self.target = target ++ self.fd = None ++ ++ def __enter__(self): ++ dnf.util.ensure_dir(os.path.dirname(self.target)) ++ self.fd = os.open(self.target, os.O_CREAT | os.O_RDWR, 0o644) ++ flags = fcntl.LOCK_SH if self.read else fcntl.LOCK_EX ++ if not self.blocking: ++ flags |= fcntl.LOCK_NB ++ try: ++ fcntl.flock(self.fd, flags) ++ except OSError as e: ++ os.close(self.fd) ++ self.fd = None ++ if e.errno == errno.EWOULDBLOCK: ++ msg = '%s already locked' % self.description ++ raise ProcessLockError(msg, -1) ++ raise ++ ++ def __exit__(self, *exc_args): ++ if self.fd is not None: ++ fcntl.flock(self.fd, fcntl.LOCK_UN) ++ os.close(self.fd) ++ self.fd = None +-- +2.54.0 + diff --git a/SOURCES/0075-Generalize-_BoolDefault-to-just-_Default-allow-choic.patch b/SOURCES/0075-Generalize-_BoolDefault-to-just-_Default-allow-choic.patch new file mode 100644 index 0000000..d8e1db0 --- /dev/null +++ b/SOURCES/0075-Generalize-_BoolDefault-to-just-_Default-allow-choic.patch @@ -0,0 +1,50 @@ +From 181a8762ccd830ebc1c7077cbbeaf621d2a0d08b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Tue, 7 Jul 2026 07:48:29 +0200 +Subject: [PATCH 2/4] Generalize _BoolDefault to just _Default, allow choices + +This is useful if we want a demand with more states than just True/False +--- + dnf/cli/demand.py | 11 +++++++++-- + 1 file changed, 9 insertions(+), 2 deletions(-) + +diff --git a/dnf/cli/demand.py b/dnf/cli/demand.py +index f82a75b1..ac19930b 100644 +--- a/dnf/cli/demand.py ++++ b/dnf/cli/demand.py +@@ -21,9 +21,10 @@ + from __future__ import unicode_literals + + +-class _BoolDefault(object): +- def __init__(self, default): ++class _Default(object): ++ def __init__(self, default, choices=None): + self.default = default ++ self.choices = choices + self._storing_name = '__%s%x' % (self.__class__.__name__, id(self)) + + def __get__(self, obj, objtype=None): +@@ -33,6 +34,8 @@ class _BoolDefault(object): + return self.default + + def __set__(self, obj, val): ++ if self.choices is not None and val not in self.choices: ++ raise ValueError('Invalid demand value: %s' % val) + objdict = obj.__dict__ + if self._storing_name in objdict: + current_val = objdict[self._storing_name] +@@ -40,6 +43,10 @@ class _BoolDefault(object): + raise AttributeError('Demand already set.') + objdict[self._storing_name] = val + ++ ++_BoolDefault = _Default ++ ++ + class DemandSheet(object): + """Collection of demands that different CLI parts have on other parts. :api""" + +-- +2.54.0 + diff --git a/SOURCES/0076-Add-clean-command-lock-demand.patch b/SOURCES/0076-Add-clean-command-lock-demand.patch new file mode 100644 index 0000000..3fa6f8c --- /dev/null +++ b/SOURCES/0076-Add-clean-command-lock-demand.patch @@ -0,0 +1,200 @@ +From ac21a60646f98d2391900f57b2cfb5af4f745f48 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Wed, 8 Jul 2026 13:47:31 +0200 +Subject: [PATCH 3/4] Add clean command lock demand + +--- + dnf/cli/demand.py | 14 +++++++ + dnf/cli/main.py | 84 +++++++++++++++++++++++----------------- + dnf/lock.py | 5 +++ + tests/cli/test_demand.py | 2 + + 4 files changed, 70 insertions(+), 35 deletions(-) + +diff --git a/dnf/cli/demand.py b/dnf/cli/demand.py +index ac19930b..2dc3a3d4 100644 +--- a/dnf/cli/demand.py ++++ b/dnf/cli/demand.py +@@ -20,6 +20,15 @@ + + from __future__ import unicode_literals + ++from enum import Enum ++ ++ ++class CleanCommandLock(Enum): ++ """Clean command lock modes. :api""" ++ NONE = 0 ++ READ = 1 ++ WRITE = 2 ++ + + class _Default(object): + def __init__(self, default, choices=None): +@@ -70,3 +79,8 @@ class DemandSheet(object): + # repositories packages (e.g. versionlock). + # If it stays None, the demands.resolving is used as a fallback. + plugin_filtering_enabled = _BoolDefault(None) ++ ++ # Determines if/how to lock the clean command lock. ++ # This lock is used to ensure clean command (which uses WRITE mode) running concurrently ++ # with another dnf process doesn't delete required data (packages). ++ clean_command_lock = _Default(CleanCommandLock.NONE, tuple(CleanCommandLock)) +diff --git a/dnf/cli/main.py b/dnf/cli/main.py +index 2a7f92d5..fcb62245 100644 +--- a/dnf/cli/main.py ++++ b/dnf/cli/main.py +@@ -24,6 +24,7 @@ from __future__ import absolute_import + from __future__ import unicode_literals + from dnf.conf import Conf + from dnf.cli.cli import Cli ++from dnf.cli.demand import CleanCommandLock + from dnf.cli.option_parser import OptionParser + from dnf.i18n import ucd + from dnf.cli.utils import show_lock_owner +@@ -33,6 +34,7 @@ import dnf.cli + import dnf.cli.cli + import dnf.cli.option_parser + import dnf.exceptions ++import dnf.lock + import dnf.i18n + import dnf.logging + import dnf.util +@@ -43,6 +45,7 @@ import logging + import os + import os.path + import sys ++from contextlib import nullcontext + + logger = logging.getLogger("dnf") + +@@ -118,45 +121,56 @@ def cli_run(cli, base): + else: + f.close() + +- try: +- cli.run() +- except dnf.exceptions.LockError: +- raise +- except (IOError, OSError) as e: +- return ex_IOError(e) ++ lock = None ++ if cli.demands.clean_command_lock == CleanCommandLock.NONE: ++ lock = nullcontext() ++ elif cli.demands.clean_command_lock == CleanCommandLock.READ: ++ lock = dnf.lock.build_clean_command_lock(base.conf.cachedir, base.conf.exit_on_lock, True) ++ elif cli.demands.clean_command_lock == CleanCommandLock.WRITE: ++ lock = dnf.lock.build_clean_command_lock(base.conf.cachedir, base.conf.exit_on_lock, False) ++ else: ++ raise RuntimeError('Invalid demands.clean_command_lock: %s' % cli.demands.clean_command_lock) + +- if cli.demands.resolving: ++ with lock: + try: +- ret = resolving(cli, base) +- except dnf.exceptions.DepsolveError as e: +- ex_Error(e) +- msg = "" +- if not cli.demands.allow_erasing and base._goal.problem_conflicts(available=True): +- msg += _("try to add '{}' to command line to replace conflicting " +- "packages").format("--allowerasing") +- if cli.base.conf.strict: +- if not msg: +- msg += _("try to add '{}' to skip uninstallable packages").format( +- "--skip-broken") +- else: +- msg += _(" or '{}' to skip uninstallable packages").format("--skip-broken") +- if cli.base.conf.best: +- prio = cli.base.conf._get_priority("best") +- if prio <= dnf.conf.PRIO_MAINCONFIG: ++ cli.run() ++ except dnf.exceptions.LockError: ++ raise ++ except (IOError, OSError) as e: ++ return ex_IOError(e) ++ ++ if cli.demands.resolving: ++ try: ++ ret = resolving(cli, base) ++ except dnf.exceptions.DepsolveError as e: ++ ex_Error(e) ++ msg = "" ++ if not cli.demands.allow_erasing and base._goal.problem_conflicts(available=True): ++ msg += _("try to add '{}' to command line to replace conflicting " ++ "packages").format("--allowerasing") ++ if cli.base.conf.strict: + if not msg: +- msg += _("try to add '{}' to use not only best candidate packages").format( +- "--nobest") ++ msg += _("try to add '{}' to skip uninstallable packages").format( ++ "--skip-broken") + else: +- msg += _(" or '{}' to use not only best candidate packages").format( +- "--nobest") +- if msg: +- logger.info("({})".format(msg)) +- raise +- if ret: +- return ret +- +- cli.command.run_transaction() +- return cli.demands.success_exit_status ++ msg += _(" or '{}' to skip uninstallable packages").format("--skip-broken") ++ if cli.base.conf.best: ++ prio = cli.base.conf._get_priority("best") ++ if prio <= dnf.conf.PRIO_MAINCONFIG: ++ if not msg: ++ msg += _("try to add '{}' to use not only best candidate packages").format( ++ "--nobest") ++ else: ++ msg += _(" or '{}' to use not only best candidate packages").format( ++ "--nobest") ++ if msg: ++ logger.info("({})".format(msg)) ++ raise ++ if ret: ++ return ret ++ ++ cli.command.run_transaction() ++ return cli.demands.success_exit_status + + + def resolving(cli, base): +diff --git a/dnf/lock.py b/dnf/lock.py +index cb189c35..aae8a51b 100644 +--- a/dnf/lock.py ++++ b/dnf/lock.py +@@ -63,6 +63,11 @@ def build_log_lock(logdir, exit_on_lock): + 'log', not exit_on_lock) + + ++def build_clean_command_lock(cachedir, exit_on_lock, read): ++ return FileLock(os.path.join(_fit_lock_dir(cachedir), 'clean_command.lock'), ++ 'clean command', not exit_on_lock, read) ++ ++ + class ProcessLock(object): + def __init__(self, target, description, blocking=False): + self.blocking = blocking +diff --git a/tests/cli/test_demand.py b/tests/cli/test_demand.py +index fc9cfcec..7f99100a 100644 +--- a/tests/cli/test_demand.py ++++ b/tests/cli/test_demand.py +@@ -21,6 +21,7 @@ from __future__ import absolute_import + from __future__ import unicode_literals + + import dnf.cli.demand ++from dnf.cli.demand import CleanCommandLock + + import tests.support + +@@ -42,6 +43,7 @@ class DemandTest(tests.support.TestCase): + self.assertFalse(demands.sack_activation) + self.assertFalse(demands.root_user) + self.assertEqual(demands.success_exit_status, 0) ++ self.assertEqual(demands.clean_command_lock, CleanCommandLock.NONE) + + def test_independence(self): + d1 = dnf.cli.demand.DemandSheet() +-- +2.54.0 + diff --git a/SOURCES/0077-Configure-demands.clean_command_lock-for-commands-th.patch b/SOURCES/0077-Configure-demands.clean_command_lock-for-commands-th.patch new file mode 100644 index 0000000..a019228 --- /dev/null +++ b/SOURCES/0077-Configure-demands.clean_command_lock-for-commands-th.patch @@ -0,0 +1,330 @@ +From 57292d5670793081a92f42e2bb4b3cafd8642d05 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Wed, 8 Jul 2026 14:19:29 +0200 +Subject: [PATCH 4/4] Configure `demands.clean_command_lock` for commands that + need it + +--- + dnf/cli/commands/__init__.py | 7 +++++++ + dnf/cli/commands/clean.py | 4 ++++ + dnf/cli/commands/distrosync.py | 2 ++ + dnf/cli/commands/downgrade.py | 2 ++ + dnf/cli/commands/group.py | 2 ++ + dnf/cli/commands/history.py | 3 +++ + dnf/cli/commands/install.py | 2 ++ + dnf/cli/commands/module.py | 4 ++++ + dnf/cli/commands/reinstall.py | 2 ++ + dnf/cli/commands/shell.py | 2 ++ + dnf/cli/commands/swap.py | 2 ++ + dnf/cli/commands/upgrade.py | 2 ++ + 12 files changed, 34 insertions(+) + +diff --git a/dnf/cli/commands/__init__.py b/dnf/cli/commands/__init__.py +index 80d67578..93d076de 100644 +--- a/dnf/cli/commands/__init__.py ++++ b/dnf/cli/commands/__init__.py +@@ -24,6 +24,7 @@ Classes for subcommands of the yum command line interface. + from __future__ import print_function + from __future__ import unicode_literals + ++from dnf.cli.demand import CleanCommandLock + from dnf.cli.option_parser import OptionParser + from dnf.i18n import _ + +@@ -340,6 +341,7 @@ class RepoPkgsCommand(Command): + demands.sack_activation = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + + def run_on_repo(self): + self.cli._populate_update_security_filter(self.opts) +@@ -392,6 +394,7 @@ class RepoPkgsCommand(Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + + def run_on_repo(self): + """Execute the command with respect to given arguments *cli_args*.""" +@@ -447,6 +450,7 @@ class RepoPkgsCommand(Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + + def run_on_repo(self): + """Execute the command with respect to given arguments *cli_args*.""" +@@ -535,6 +539,7 @@ class RepoPkgsCommand(Command): + demands.sack_activation = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + + def _replace(self, pkg_spec, reponame): + """Synchronize a package with another repository or remove it.""" +@@ -597,6 +602,7 @@ class RepoPkgsCommand(Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + + def run_on_repo(self): + """Execute the command with respect to given arguments *cli_args*.""" +@@ -686,6 +692,7 @@ class RepoPkgsCommand(Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + + def run_on_repo(self): + """Execute the command with respect to given arguments *cli_args*.""" +diff --git a/dnf/cli/commands/clean.py b/dnf/cli/commands/clean.py +index 77f83f02..df14ebd7 100644 +--- a/dnf/cli/commands/clean.py ++++ b/dnf/cli/commands/clean.py +@@ -21,6 +21,7 @@ + from __future__ import absolute_import + from __future__ import unicode_literals + from dnf.cli import commands ++from dnf.cli.demand import CleanCommandLock + from dnf.i18n import _, P_ + from dnf.yum import misc + +@@ -92,6 +93,9 @@ class CleanCommand(commands.Command): + choices=_CACHE_TYPES.keys(), + help=_('Metadata type to clean')) + ++ def configure(self): ++ self.cli.demands.clean_command_lock = CleanCommandLock.WRITE ++ + def run(self): + cachedir = self.base.conf.cachedir + md_lock = dnf.lock.build_metadata_lock(cachedir, True) +diff --git a/dnf/cli/commands/distrosync.py b/dnf/cli/commands/distrosync.py +index 3d472e5f..7896f61f 100644 +--- a/dnf/cli/commands/distrosync.py ++++ b/dnf/cli/commands/distrosync.py +@@ -20,6 +20,7 @@ + + from __future__ import absolute_import + from dnf.cli import commands ++from dnf.cli.demand import CleanCommandLock + from dnf.i18n import _ + + +@@ -41,6 +42,7 @@ class DistroSyncCommand(commands.Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + commands._checkGPGKey(self.base, self.cli) + commands._checkEnabledRepo(self.base, self.opts.package) + +diff --git a/dnf/cli/commands/downgrade.py b/dnf/cli/commands/downgrade.py +index 9e27962b..187e56d5 100644 +--- a/dnf/cli/commands/downgrade.py ++++ b/dnf/cli/commands/downgrade.py +@@ -21,6 +21,7 @@ + from __future__ import absolute_import + from __future__ import unicode_literals + from dnf.cli import commands ++from dnf.cli.demand import CleanCommandLock + from dnf.cli.option_parser import OptionParser + from dnf.i18n import _ + +@@ -44,6 +45,7 @@ class DowngradeCommand(commands.Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + + commands._checkGPGKey(self.base, self.cli) + if not self.opts.filenames: +diff --git a/dnf/cli/commands/group.py b/dnf/cli/commands/group.py +index 6de8baa1..9375f81d 100644 +--- a/dnf/cli/commands/group.py ++++ b/dnf/cli/commands/group.py +@@ -22,6 +22,7 @@ from __future__ import absolute_import + from __future__ import unicode_literals + from dnf.comps import CompsQuery + from dnf.cli import commands ++from dnf.cli.demand import CleanCommandLock + from dnf.i18n import _, ucd + + import libdnf.transaction +@@ -363,6 +364,7 @@ class GroupCommand(commands.Command): + + if cmd in ('install', 'upgrade'): + commands._checkGPGKey(self.base, self.cli) ++ demands.clean_command_lock = CleanCommandLock.READ + + def run(self): + cmd = self.opts.subcmd +diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py +index 21d04a1a..d01858b3 100644 +--- a/dnf/cli/commands/history.py ++++ b/dnf/cli/commands/history.py +@@ -25,6 +25,7 @@ import hawkey + from dnf.i18n import _, ucd + from dnf.cli import commands + from dnf.transaction_sr import TransactionReplay, serialize_transaction ++from dnf.cli.demand import CleanCommandLock + + import dnf.cli + import dnf.exceptions +@@ -110,6 +111,7 @@ class HistoryCommand(commands.Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + + # Override configuration options that affect how the transaction is resolved + self.base.conf.clean_requirements_on_remove = False +@@ -124,6 +126,7 @@ class HistoryCommand(commands.Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + + self._require_one_transaction_id = True + if not self.opts.transactions: +diff --git a/dnf/cli/commands/install.py b/dnf/cli/commands/install.py +index e13cc669..59bc2991 100644 +--- a/dnf/cli/commands/install.py ++++ b/dnf/cli/commands/install.py +@@ -28,6 +28,7 @@ import hawkey + + import dnf.exceptions + from dnf.cli import commands ++from dnf.cli.demand import CleanCommandLock + from dnf.cli.option_parser import OptionParser + from dnf.i18n import _ + +@@ -62,6 +63,7 @@ class InstallCommand(commands.Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + commands._checkGPGKey(self.base, self.cli) + if not self.opts.filenames: + commands._checkEnabledRepo(self.base) +diff --git a/dnf/cli/commands/module.py b/dnf/cli/commands/module.py +index 0f584f90..9b55de80 100644 +--- a/dnf/cli/commands/module.py ++++ b/dnf/cli/commands/module.py +@@ -20,6 +20,7 @@ + from __future__ import print_function + + from dnf.cli import commands, CliError ++from dnf.cli.demand import CleanCommandLock + from dnf.i18n import _ + from dnf.module.exceptions import NoModuleException + from dnf.util import logger +@@ -210,6 +211,7 @@ class ModuleCommand(commands.Command): + demands.sack_activation = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + + def run_on_module(self): + try: +@@ -231,6 +233,7 @@ class ModuleCommand(commands.Command): + demands.sack_activation = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + + def run_on_module(self): + module_specs = self.module_base.upgrade(self.opts.module_spec) +@@ -285,6 +288,7 @@ class ModuleCommand(commands.Command): + demands.sack_activation = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + self.base.conf.module_stream_switch = True + + def run_on_module(self): +diff --git a/dnf/cli/commands/reinstall.py b/dnf/cli/commands/reinstall.py +index 2b3ceac7..9d95cea0 100644 +--- a/dnf/cli/commands/reinstall.py ++++ b/dnf/cli/commands/reinstall.py +@@ -21,6 +21,7 @@ + from __future__ import absolute_import + from __future__ import unicode_literals + from dnf.cli import commands ++from dnf.cli.demand import CleanCommandLock + from dnf.cli.option_parser import OptionParser + from dnf.i18n import _ + +@@ -54,6 +55,7 @@ class ReinstallCommand(commands.Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + commands._checkGPGKey(self.base, self.cli) + if not self.opts.filenames: + commands._checkEnabledRepo(self.base) +diff --git a/dnf/cli/commands/shell.py b/dnf/cli/commands/shell.py +index 18c886ff..f2a1bf32 100644 +--- a/dnf/cli/commands/shell.py ++++ b/dnf/cli/commands/shell.py +@@ -19,6 +19,7 @@ + # + + from dnf.cli import commands ++from dnf.cli.demand import CleanCommandLock + from dnf.i18n import _, ucd + + import dnf.util +@@ -39,6 +40,7 @@ class ShellDemandSheet(object): + resolving = True + root_user = True + sack_activation = True ++ clean_command_lock = CleanCommandLock.READ + + + class ShellCommand(commands.Command, cmd.Cmd): +diff --git a/dnf/cli/commands/swap.py b/dnf/cli/commands/swap.py +index d44b3f4f..e61a24dc 100644 +--- a/dnf/cli/commands/swap.py ++++ b/dnf/cli/commands/swap.py +@@ -20,6 +20,7 @@ from __future__ import absolute_import + from __future__ import unicode_literals + from dnf.i18n import _ + from dnf.cli import commands ++from dnf.cli.demand import CleanCommandLock + + import dnf.util + import logging +@@ -47,6 +48,7 @@ class SwapCommand(commands.Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + commands._checkGPGKey(self.base, self.cli) + commands._checkEnabledRepo(self.base, [self.opts.install_spec]) + +diff --git a/dnf/cli/commands/upgrade.py b/dnf/cli/commands/upgrade.py +index 15e63136..fea0cb2d 100644 +--- a/dnf/cli/commands/upgrade.py ++++ b/dnf/cli/commands/upgrade.py +@@ -26,6 +26,7 @@ import logging + import dnf.exceptions + import dnf.base + from dnf.cli import commands ++from dnf.cli.demand import CleanCommandLock + from dnf.cli.option_parser import OptionParser + from dnf.i18n import _ + +@@ -56,6 +57,7 @@ class UpgradeCommand(commands.Command): + demands.available_repos = True + demands.resolving = True + demands.root_user = True ++ demands.clean_command_lock = CleanCommandLock.READ + commands._checkGPGKey(self.base, self.cli) + if not self.opts.filenames: + commands._checkEnabledRepo(self.base) +-- +2.54.0 + diff --git a/SPECS/dnf.spec b/SPECS/dnf.spec index 1ff455a..7bb6733 100644 --- a/SPECS/dnf.spec +++ b/SPECS/dnf.spec @@ -73,7 +73,7 @@ It supports RPMs, modules and comps groups & environments. Name: dnf Version: 4.14.0 -Release: 33%{?dist} +Release: 37%{?dist} Summary: %{pkg_summary} # For a breakdown of the licensing, see PACKAGE-LICENSING License: GPLv2+ @@ -148,6 +148,14 @@ Patch66: 0066-Document-how-releasever-releasever_-major-minor-affe.patch Patch67: 0067-Move-releasever_minor-setter-docstring-to-the-correc.patch Patch68: 0068-automatic-Expand-email_to-in-command_email-emitter-t.patch Patch69: 0069-autoremove-warn-and-skip-dangling-protected-dependen.patch +Patch70: 0070-bootc-unlock-only-if-usr-is-read-only.patch +Patch71: 0071-bootc-Call-make_writable-when-DeploymentUnlockedStat.patch +Patch72: 0072-Preserve-ACL-when-rotating-logs.patch +Patch73: 0073-history-info-Fix-Persistence-display-for-interval-qu.patch +Patch74: 0074-Add-FileLock-for-read-write-and-blocking-non-blockin.patch +Patch75: 0075-Generalize-_BoolDefault-to-just-_Default-allow-choic.patch +Patch76: 0076-Add-clean-command-lock-demand.patch +Patch77: 0077-Configure-demands.clean_command_lock-for-commands-th.patch BuildArch: noarch BuildRequires: cmake @@ -454,6 +462,18 @@ popd # bootc subpackage does not include any files %changelog +* Tue Jul 14 2026 Ales Matej - 4.14.0-37 +- Add dedicated lock for clean command (RHEL-83124) + +* Wed May 20 2026 Evan Goode - 4.14.0-36 +- history info: Fix Persistence display for interval queries (RHEL-154737) + +* Thu Mar 26 2026 Petr Pisar - 4.14.0-35 +- Preserve ACL when rotating logs (RHEL-122013) + +* Wed Mar 25 2026 Evan Goode - 4.14.0-34 +- bootc: unlock only if /usr is read-only (RHEL-138512) + * Wed Feb 11 2026 Ales Matej - 4.14.0-33 - autoremove: when a dangling protected dependency is found produce a wanrning and skip it (RHEL-76112)