commit
725165416f
@ -0,0 +1 @@
|
||||
71cc8d130f8f7327f57e9b96a271a0f9a18e7e0e SOURCES/dnf-4.12.0.tar.gz
|
@ -0,0 +1 @@
|
||||
SOURCES/dnf-4.12.0.tar.gz
|
@ -0,0 +1,317 @@
|
||||
From 5ce5ed1ea08ad6e198c1c1642c4d9ea2db6eab86 Mon Sep 17 00:00:00 2001
|
||||
From: Laszlo Ersek <lersek@redhat.com>
|
||||
Date: Sun, 24 Apr 2022 09:08:28 +0200
|
||||
Subject: [PATCH] Base.reset: plug (temporary) leak of libsolv's page file
|
||||
descriptors
|
||||
|
||||
Consider the following call paths (mixed Python and C), extending from
|
||||
livecd-creator down to libsolv:
|
||||
|
||||
main [livecd-tools/tools/livecd-creator]
|
||||
install() [livecd-tools/imgcreate/creator.py]
|
||||
fill_sack() [dnf/dnf/base.py]
|
||||
_add_repo_to_sack() [dnf/dnf/base.py]
|
||||
load_repo() [libdnf/python/hawkey/sack-py.cpp]
|
||||
dnf_sack_load_repo() [libdnf/libdnf/dnf-sack.cpp]
|
||||
write_main() [libdnf/libdnf/dnf-sack.cpp]
|
||||
repo_add_solv() [libsolv/src/repo_solv.c]
|
||||
repopagestore_read_or_setup_pages() [libsolv/src/repopage.c]
|
||||
dup()
|
||||
write_ext() [libdnf/libdnf/dnf-sack.cpp]
|
||||
repo_add_solv() [libsolv/src/repo_solv.c]
|
||||
repopagestore_read_or_setup_pages() [libsolv/src/repopage.c]
|
||||
dup()
|
||||
|
||||
The dup() calls create the following file descriptors (output from
|
||||
"lsof"):
|
||||
|
||||
> COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
|
||||
> python3 6500 root 7r REG 8,1 25320727 395438 /var/tmp/imgcreate-mytcghah/install_root/var/cache/dnf/fedora.solv (deleted)
|
||||
> python3 6500 root 8r REG 8,1 52531426 395450 /var/tmp/imgcreate-mytcghah/install_root/var/cache/dnf/fedora-filenames.solvx
|
||||
|
||||
These file descriptors are *owned* by the DnfSack object (which is derived
|
||||
from GObject), as follows:
|
||||
|
||||
sack->priv->pool->repos[1]->repodata[1]->store.pagefd = 7
|
||||
sack->priv->pool->repos[1]->repodata[2]->store.pagefd = 8
|
||||
^ ^ ^ ^ ^ ^ ^
|
||||
| | | | | | |
|
||||
| | | | | | int
|
||||
| | | | | Repopagestore [libsolv/src/repopage.h]
|
||||
| | | | Repodata [libsolv/src/repodata.h]
|
||||
| | | struct s_Repo [libsolv/src/repo.h]
|
||||
| | struct s_Pool (aka Pool) [libsolv/src/pool.h]
|
||||
| DnfSackPrivate [libdnf/libdnf/dnf-sack.cpp]
|
||||
DnfSack [libdnf/libdnf/dnf-sack.h]
|
||||
|
||||
The file descriptors are *supposed* to be closed on the following call
|
||||
path:
|
||||
|
||||
main [livecd-tools/tools/livecd-creator]
|
||||
install() [livecd-tools/imgcreate/creator.py]
|
||||
close() [livecd-tools/imgcreate/dnfinst.py]
|
||||
close() [dnf/dnf/base.py]
|
||||
reset() [dnf/dnf/base.py]
|
||||
_sack = None
|
||||
_goal = None
|
||||
_transaction = None
|
||||
...
|
||||
dnf_sack_finalize() [libdnf/libdnf/dnf-sack.cpp]
|
||||
pool_free() [libsolv/src/pool.c]
|
||||
pool_freeallrepos() [libsolv/src/pool.c]
|
||||
repo_freedata() [libsolv/src/repo.c]
|
||||
repodata_freedata() [libsolv/src/repodata.c]
|
||||
repopagestore_free() [libsolv/src/repopage.c]
|
||||
close()
|
||||
|
||||
Namely, when dnf.Base.reset() [dnf/dnf/base.py] is called with (sack=True,
|
||||
goal=True), the reference counts of the objects pointed to by the "_sack",
|
||||
"_goal" and "_transaction" fields are supposed to reach zero, and then, as
|
||||
part of the DnfSack object's finalization, the libsolv file descriptors
|
||||
are supposed to be closed.
|
||||
|
||||
Now, while this *may* happen immediately in dnf.Base.reset(), it may as
|
||||
well not. The reason is that there is a multitude of *circular references*
|
||||
between DnfSack and the packages that it contains. When dnf.Base.reset()
|
||||
is entered, we have the following picture:
|
||||
|
||||
_sack _goal
|
||||
| |
|
||||
v v
|
||||
+----------------+ +-------------+
|
||||
| DnfSack object | <--- | Goal object |
|
||||
+----------------+ +-------------+
|
||||
|^ |^ |^
|
||||
|| || ||
|
||||
|| || ||
|
||||
+--||----||----||---+
|
||||
| v| v| v| | <-- _transaction
|
||||
| Pkg1 Pkg2 PkgN |
|
||||
| |
|
||||
| Transaction oject |
|
||||
+-------------------+
|
||||
|
||||
That is, the reference count of the DnfSack object is (1 + 1 + N), where N
|
||||
is the number of packages in the transaction. Details:
|
||||
|
||||
(a) The first reference comes from the "_sack" field, established like
|
||||
this:
|
||||
|
||||
main [livecd-tools/tools/livecd-creator]
|
||||
install() [livecd-tools/imgcreate/creator.py]
|
||||
fill_sack() [dnf/dnf/base.py]
|
||||
_build_sack() [dnf/dnf/sack.py]
|
||||
Sack()
|
||||
sack_init() [libdnf/python/hawkey/sack-py.cpp]
|
||||
dnf_sack_new() [libdnf/libdnf/dnf-sack.cpp]
|
||||
|
||||
(b) The second reference on the DnfSack object comes from "_goal":
|
||||
|
||||
main [livecd-tools/tools/livecd-creator]
|
||||
install() [livecd-tools/imgcreate/creator.py]
|
||||
fill_sack() [dnf/dnf/base.py]
|
||||
_goal = Goal(_sack)
|
||||
goal_init() [libdnf/python/hawkey/goal-py.cpp]
|
||||
Py_INCREF(_sack)
|
||||
|
||||
(c) Then there is one reference to "_sack" *per package* in the
|
||||
transaction:
|
||||
|
||||
main [livecd-tools/tools/livecd-creator]
|
||||
install() [livecd-tools/imgcreate/creator.py]
|
||||
runInstall() [livecd-tools/imgcreate/dnfinst.py]
|
||||
resolve() [dnf/dnf/base.py]
|
||||
_goal2transaction() [dnf/dnf/base.py]
|
||||
list_installs() [libdnf/python/hawkey/goal-py.cpp]
|
||||
list_generic() [libdnf/python/hawkey/goal-py.cpp]
|
||||
packagelist_to_pylist() [libdnf/python/hawkey/iutil-py.cpp]
|
||||
new_package() [libdnf/python/hawkey/sack-py.cpp]
|
||||
Py_BuildValue()
|
||||
ts.add_install()
|
||||
|
||||
list_installs() creates a list of packages that need to be installed
|
||||
by DNF. Inside the loop in packagelist_to_pylist(), which constructs
|
||||
the elements of that list, Py_BuildValue() is called with the "O"
|
||||
format specifier, and that increases the reference count on "_sack".
|
||||
|
||||
Subsequently, in the _goal2transaction() method, we iterate over the
|
||||
package list created by list_installs(), and add each package to the
|
||||
transaction (ts.add_install()). After _goal2transaction() returns,
|
||||
this transaction is assigned to "self._transaction" in resolve(). This
|
||||
is where the last N (back-)references on the DnfSack object come from.
|
||||
|
||||
(d) Now, to quote the defintion of the DnfSack object
|
||||
("libdnf/docs/hawkey/tutorial-py.rst"):
|
||||
|
||||
> *Sack* is an abstraction for a collection of packages.
|
||||
|
||||
That's why the DnfSack object references all the Pkg1 through PkgN
|
||||
packages.
|
||||
|
||||
So, when the dnf.Base.reset() method completes, the picture changes like
|
||||
this:
|
||||
|
||||
_sack _goal
|
||||
| |
|
||||
-- [CUT] -- -- [CUT] --
|
||||
| |
|
||||
v | v
|
||||
+----------------+ [C] +-------------+
|
||||
| DnfSack object | <-[U]- | Goal object |
|
||||
+----------------+ [T] +-------------+
|
||||
|^ |^ |^ |
|
||||
|| || ||
|
||||
|| || || |
|
||||
+--||----||----||---+ [C]
|
||||
| v| v| v| | <--[U]-- _transaction
|
||||
| Pkg1 Pkg2 PkgN | [T]
|
||||
| | |
|
||||
| Transaction oject |
|
||||
+-------------------+
|
||||
|
||||
and we are left with N reference cycles (one between each package and the
|
||||
same DnfSack object).
|
||||
|
||||
This set of cycles can only be cleaned up by Python's generational garbage
|
||||
collector <https://stackify.com/python-garbage-collection/>. The GC will
|
||||
collect the DnfSack object, and consequently close the libsolv page file
|
||||
descriptors via dnf_sack_finalize() -- but garbage collection will happen
|
||||
*only eventually*, unpredictably.
|
||||
|
||||
This means that the dnf.Base.reset() method breaks its interface contract:
|
||||
|
||||
> Make the Base object forget about various things.
|
||||
|
||||
because the libsolv file descriptors can (and frequently do, in practice)
|
||||
survive dnf.Base.reset().
|
||||
|
||||
In general, as long as the garbage collector only tracks process-private
|
||||
memory blocks, there's nothing wrong; however, file descriptors are
|
||||
visible to the kernel. When dnf.Base.reset() *temporarily* leaks file
|
||||
descriptors as explained above, then immediately subsequent operations
|
||||
that depend on those file descriptors having been closed, can fail.
|
||||
|
||||
An example is livecd-creator's unmounting of:
|
||||
|
||||
/var/tmp/imgcreate-mytcghah/install_root/var/cache/dnf
|
||||
|
||||
which the kernel refuses, due to libsolv's still open file descriptors
|
||||
pointing into that filesystem:
|
||||
|
||||
> umount: /var/tmp/imgcreate-mytcghah/install_root/var/cache/dnf: target
|
||||
> is busy.
|
||||
> Unable to unmount /var/tmp/imgcreate-mytcghah/install_root/var/cache/dnf
|
||||
> normally, using lazy unmount
|
||||
|
||||
(Unfortunately, the whole lazy umount idea is misguided in livecd-tools;
|
||||
it's a misfeature that should be removed, as it permits the corruption of
|
||||
the loop-backed filesystem. Now that the real bug is being fixed in DNF,
|
||||
lazy umount is not needed as a (broken) workaround in livecd-tools. But
|
||||
that's a separate patch for livecd-tools:
|
||||
<https://github.com/livecd-tools/livecd-tools/pull/227>.)
|
||||
|
||||
Plug the fd leak by forcing a garbage collection in dnf.Base.reset()
|
||||
whenever we cut the "_sack", "_goal" and "_transaction" links -- that is,
|
||||
when the "sack" and "goal" parameters are True.
|
||||
|
||||
Note that precisely due to the unpredictable behavior of the garbage
|
||||
collector, reproducing the bug may prove elusive. In order to reproduce it
|
||||
deterministically, through usage with livecd-creator, disabling automatic
|
||||
garbage collection with the following patch (for livecd-tools) is
|
||||
sufficient:
|
||||
|
||||
> diff --git a/tools/livecd-creator b/tools/livecd-creator
|
||||
> index 291de10cbbf9..8d2c740c238b 100755
|
||||
> --- a/tools/livecd-creator
|
||||
> +++ b/tools/livecd-creator
|
||||
> @@ -31,6 +31,8 @@ from dnf.exceptions import Error as DnfBaseError
|
||||
> import imgcreate
|
||||
> from imgcreate.errors import KickstartError
|
||||
>
|
||||
> +import gc
|
||||
> +
|
||||
> class Usage(Exception):
|
||||
> def __init__(self, msg = None, no_error = False):
|
||||
> Exception.__init__(self, msg, no_error)
|
||||
> @@ -261,5 +263,6 @@ def do_nss_libs_hack():
|
||||
> return hack
|
||||
>
|
||||
> if __name__ == "__main__":
|
||||
> + gc.disable()
|
||||
> hack = do_nss_libs_hack()
|
||||
> sys.exit(main())
|
||||
|
||||
Also note that you need to use livecd-tools at git commit 4afde9352e82 or
|
||||
later, for this fix to make any difference: said commit fixes a different
|
||||
(independent) bug in livecd-tools that produces identical symptoms, but
|
||||
from a different origin. In other words, if you don't have commit
|
||||
4afde9352e82 in your livecd-tools install, then said bug in livecd-tools
|
||||
will mask this DNF fix.
|
||||
|
||||
Signed-off-by: Laszlo Ersek <lersek@redhat.com>
|
||||
---
|
||||
dnf/base.py | 41 +++++++++++++++++++++++++++++++++++++++++
|
||||
1 file changed, 41 insertions(+)
|
||||
|
||||
diff --git a/dnf/base.py b/dnf/base.py
|
||||
index caace028..520574b4 100644
|
||||
--- a/dnf/base.py
|
||||
+++ b/dnf/base.py
|
||||
@@ -72,6 +72,7 @@ import dnf.transaction
|
||||
import dnf.util
|
||||
import dnf.yum.rpmtrans
|
||||
import functools
|
||||
+import gc
|
||||
import hawkey
|
||||
import itertools
|
||||
import logging
|
||||
@@ -569,6 +570,46 @@ class Base(object):
|
||||
self._comps_trans = dnf.comps.TransactionBunch()
|
||||
self._transaction = None
|
||||
self._update_security_filters = []
|
||||
+ if sack and goal:
|
||||
+ # We've just done this, above:
|
||||
+ #
|
||||
+ # _sack _goal
|
||||
+ # | |
|
||||
+ # -- [CUT] -- -- [CUT] --
|
||||
+ # | |
|
||||
+ # v | v
|
||||
+ # +----------------+ [C] +-------------+
|
||||
+ # | DnfSack object | <-[U]- | Goal object |
|
||||
+ # +----------------+ [T] +-------------+
|
||||
+ # |^ |^ |^ |
|
||||
+ # || || ||
|
||||
+ # || || || |
|
||||
+ # +--||----||----||---+ [C]
|
||||
+ # | v| v| v| | <--[U]-- _transaction
|
||||
+ # | Pkg1 Pkg2 PkgN | [T]
|
||||
+ # | | |
|
||||
+ # | Transaction oject |
|
||||
+ # +-------------------+
|
||||
+ #
|
||||
+ # At this point, the DnfSack object would be released only
|
||||
+ # eventually, by Python's generational garbage collector, due to the
|
||||
+ # cyclic references DnfSack<->Pkg1 ... DnfSack<->PkgN.
|
||||
+ #
|
||||
+ # The delayed release is a problem: the DnfSack object may
|
||||
+ # (indirectly) own "page file" file descriptors in libsolv, via
|
||||
+ # libdnf. For example,
|
||||
+ #
|
||||
+ # sack->priv->pool->repos[1]->repodata[1]->store.pagefd = 7
|
||||
+ # sack->priv->pool->repos[1]->repodata[2]->store.pagefd = 8
|
||||
+ #
|
||||
+ # These file descriptors are closed when the DnfSack object is
|
||||
+ # eventually released, that is, when dnf_sack_finalize() (in libdnf)
|
||||
+ # calls pool_free() (in libsolv).
|
||||
+ #
|
||||
+ # We need that to happen right now, as callers may want to unmount
|
||||
+ # the filesystems which those file descriptors refer to immediately
|
||||
+ # after reset() returns. Therefore, force a garbage collection here.
|
||||
+ gc.collect()
|
||||
|
||||
def _closeRpmDB(self):
|
||||
"""Closes down the instances of rpmdb that could be open."""
|
||||
--
|
||||
2.35.1
|
||||
|
@ -0,0 +1,64 @@
|
||||
From f32eff294aecaac0fd71cd8888a25fa7929460b9 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= <amatej@redhat.com>
|
||||
Date: Mon, 4 Jul 2022 09:43:25 +0200
|
||||
Subject: [PATCH] Add only relevant pkgs to upgrade transaction (RhBug:2097757)
|
||||
|
||||
https://bugzilla.redhat.com/show_bug.cgi?id=2097757
|
||||
|
||||
Without this patch dnf can create the following transaction during dnf upgrade --security when there is an advisory for B-2-2:
|
||||
|
||||
```
|
||||
repo @System 0 testtags <inline>
|
||||
#>=Pkg: A 1 1 x86_64
|
||||
#>=Pkg: B 1 1 x86_64
|
||||
#>=Req: A = 1-1
|
||||
|
||||
repo available 0 testtags <inline>
|
||||
#>=Pkg: A 2 2 x86_64
|
||||
#>=Pkg: B 2 2 x86_64
|
||||
#>=Req: A = 2-2
|
||||
system x86_64 rpm @System
|
||||
job update oneof A-1-1.x86_64@@System B-2-2.x86_64@available [targeted,setevr,setarch]
|
||||
result transaction,problems
|
||||
```
|
||||
|
||||
Problem is that without forcebest nothing gets upgraded despite the available advisory and --security switch.
|
||||
|
||||
This can also be seen in CI test case: rpm-software-management/ci-dnf-stack#1130
|
||||
---
|
||||
dnf/base.py | 19 ++++++++++++++++++-
|
||||
1 file changed, 18 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/dnf/base.py b/dnf/base.py
|
||||
index caace028..92fb3bd0 100644
|
||||
--- a/dnf/base.py
|
||||
+++ b/dnf/base.py
|
||||
@@ -2118,7 +2118,24 @@ class Base(object):
|
||||
query.filterm(reponame=reponame)
|
||||
query = self._merge_update_filters(query, pkg_spec=pkg_spec, upgrade=True)
|
||||
if query:
|
||||
- query = query.union(installed_query.latest())
|
||||
+ # Given that we use libsolv's targeted transactions, we need to ensure that the transaction contains both
|
||||
+ # the new targeted version and also the current installed version (for the upgraded package). This is
|
||||
+ # because if it only contained the new version, libsolv would decide to reinstall the package even if it
|
||||
+ # had just a different buildtime or vendor but the same version
|
||||
+ # (https://github.com/openSUSE/libsolv/issues/287)
|
||||
+ # - In general, the query already contains both the new and installed versions but not always.
|
||||
+ # If repository-packages command is used, the installed packages are filtered out because they are from
|
||||
+ # the @system repo. We need to add them back in.
|
||||
+ # - However we need to add installed versions of just the packages that are being upgraded. We don't want
|
||||
+ # to add all installed packages because it could increase the number of solutions for the transaction
|
||||
+ # (especially without --best) and since libsolv prefers the smallest possible upgrade it could result
|
||||
+ # in no upgrade even if there is one available. This is a problem in general but its critical with
|
||||
+ # --security transactions (https://bugzilla.redhat.com/show_bug.cgi?id=2097757)
|
||||
+ # - We want to add only the latest versions of installed packages, this is specifically for installonly
|
||||
+ # packages. Otherwise if for example kernel-1 and kernel-3 were installed and present in the
|
||||
+ # transaction libsolv could decide to install kernel-2 because it is an upgrade for kernel-1 even
|
||||
+ # though we don't want it because there already is a newer version present.
|
||||
+ query = query.union(installed_query.latest().filter(name=[pkg.name for pkg in query]))
|
||||
sltr = dnf.selector.Selector(self.sack)
|
||||
sltr.set(pkg=query)
|
||||
self._goal.upgrade(select=sltr)
|
||||
--
|
||||
2.36.1
|
||||
|
@ -0,0 +1,37 @@
|
||||
From 776241568cb10e3a671c574b25e06b63d86e7ac0 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= <amatej@redhat.com>
|
||||
Date: Mon, 4 Jul 2022 09:46:29 +0200
|
||||
Subject: [PATCH] Use `installed_all` because `installed_query` is filtered
|
||||
user input
|
||||
|
||||
`installed_query` could be missing packages. If we specify we want to
|
||||
upgrade a specific nevra that is not yet installed, then `installed_query`
|
||||
is empty because it is based on user input, but there could be other
|
||||
versions of the pkg installed.
|
||||
|
||||
Eg: if kernel-1 and kernel-3 are installed and we specify we want to
|
||||
upgrade kernel-2, nothing should be done because we already have higher
|
||||
version, but now `installed_query` would be empty and kernel-2 would be
|
||||
installed.
|
||||
|
||||
Therefore, we need to use `installed_all`.
|
||||
---
|
||||
dnf/base.py | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/dnf/base.py b/dnf/base.py
|
||||
index 92fb3bd0..1b0f07ed 100644
|
||||
--- a/dnf/base.py
|
||||
+++ b/dnf/base.py
|
||||
@@ -2135,7 +2135,7 @@ class Base(object):
|
||||
# packages. Otherwise if for example kernel-1 and kernel-3 were installed and present in the
|
||||
# transaction libsolv could decide to install kernel-2 because it is an upgrade for kernel-1 even
|
||||
# though we don't want it because there already is a newer version present.
|
||||
- query = query.union(installed_query.latest().filter(name=[pkg.name for pkg in query]))
|
||||
+ query = query.union(installed_all.latest().filter(name=[pkg.name for pkg in query]))
|
||||
sltr = dnf.selector.Selector(self.sack)
|
||||
sltr.set(pkg=query)
|
||||
self._goal.upgrade(select=sltr)
|
||||
--
|
||||
2.36.1
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue