Hi Daniel,

On Mon, Apr 28, 2025 at 05:20:31PM +0200, Daniel Baumann wrote:
> On 4/28/25 14:58, Helmut Grohne wrote:
> > I am sorry to tell you that the brittle /usr-move mitigations broke
> > again.
> 
> no worries, I'm honestly so lucky that you're able and willing to help, I'll
> gladly wait for your patch.

Well, it was me who got you into this misery as I claimed that
duplicating diversions would allow us to move all the files from / to
/usr. How foolish of me.

Evidently, I am unable to fully grasp the complexity involved. In the
hope of not screwing up again, I started writing more extensive tests
than last time.

Testing
~~~~~~~

You find a testcase.sh and a Makefile.test attached. These are based on
my earlier work in #1059534. Compared to back then, I expanded the test
cases significantly. The original method spelled out cases as a way of
installing or removing packages before or after upgrading to trixie.
I've now taken this down to the dpkg level driving unpacks and
configurations directly in order to exercise more control. In
particular, I am also testing the wicked case of temporarily removing
zutils as part of the upgrade and in doing so, I can reproduce the
reported problem. Let me quote the test matrix from Makefile.test as
it's important that this matrix is exhaustive.

        unpgzip-confgzip-unpzutils-confzutils-rmzutils
        unpgzip-confgzip-unpzutils-rmzutils
        unpgzip-unpzutils-confgzip-confzutils-rmzutils
        unpgzip-unpzutils-confgzip-rmzutils
        unpgzip-unpzutils-confzutils-confgzip-rmzutils
        unpgzip-unpzutils-confzutils-rmzutils-confgzip
        unpgzip-unpzutils-rmzutils-confgzip
        unpzutils-unpgzip-confgzip-confzutils-rmzutils
        unpzutils-unpgzip-confzutils-confgzip-rmzutils
        unpzutils-unpgzip-rmzutils-confgzip
        unpzutils-confzutils-unpgzip-confgzip-rmzutils
        unpzutils-confzutils-rmzutils
        unpzutils-rmzutils-unpgzip-confgzip
        withzutils-deinstzutils-unpgzip-rmzutils-confgzip
        
withzutils-deinstzutils-unpgzip-rmzutils-unpzutils-confgzip-confzutils-rmzutils
        
withzutils-deinstzutils-unpgzip-rmzutils-unpzutils-confzutils-confgzip-rmzutils
        withzutils-unpzutils-unpgzip-confgzip-confzutils-rmzutils
        withzutils-unpzutils-unpgzip-confzutils-confgzip-rmzutils
        withzutils-unpzutils-unpgzip-rmzutils-confgzip
        withzutils-unpzutils-confzutils-unpgzip-confgzip-rmzutils

We always start with a bookworm installation. As gzip is essential,
that's always included. zutils is installed if the case starts with
"withzutils". Then the sequence of events is executed. "unp*" means
unpacking the relevant package (from trixie). Likewise "conf*" and "rm*"
refer to configuring and removing the package. The failure case involves
scheduling a package for removal and that's what "deinst*" does. If you
can think of any valid interaction that's not listed here, please tell.
For instance, we see no withzutils-unpgzip-*, because trixie's gzip
Conflicts with bookworm's zutils and therefore we have to upgrade
zutils (unpzutils), remove zutils (skip withzutils) or schedule
deinstallation of zutils (deinstzutils) before unpacking trixie's gzip.

Fixing
~~~~~~

The actual changes are significant. For one thing, I argued that zutils
should Breaks gzip to ensure ordering of postinst. Not sure how I got
there as there is no postinst. Meanwhile, that Breaks combined with
gzip's Conflicts causes apt to invoke that wicked code path of
temporarily uninstalling zutils that presently is broken. However, the
preinst argues that wrongly renaming is ok, because gzip will be
upgraded and fix that. Once dropping that Breaks, we may no longer
wrongly rename.

That leads us to preinst changes. How we have to rename depends on
whether gzip installs its tools in /bin or /usr/bin as those diversions
have differing targets beyond aliasing. One option would be to
repeatedly dpkg-query -S those files, but each such invocation takes
0.2s even on fast CPUs and a hot cache. Instead, I propose examining
the gzip version to compute the intended location.

Then, an initial installation may be an upgrade performed by temporary
removal. In other words, gzip may have diverted its tools. We need to
actively undo that diversion.

As mentioned earlier, we should not be using --rename, so I'm changing
that to --no-rename and as a result I have to do the renaming myself.
The original location simply is the result of dpkg-divert --truename as
the removal of gzip's diversion also uses --no-rename. The destination
location depends on whether gzip installed aliased or canonical (i.e.
the gzip version).

While looking at the code, I noticed that the upgrade path was
implemented with --no-rename as well, but not doing any rename while it
really should. I'm adding that missing rename, but I also figured why
that didn't cause any problems. Due to gzip's Conflicts that branch
really should be dead code.

Last but not least, I'm being more strict on certain conditions and have
preinst abort when it is faced with unexpected situations.

Retrospective
~~~~~~~~~~~~~

How did we get into this last failure? The problem existed in the
initial move as the test suite did not cover the relevant case. It was
driven by apt and at that time apt did not exercise the temporary
removal. The addition of Breaks via #1092737 caused apt to choose the
temporary removal path and that's why it is now practically broken.

I hope to find someone who can review this work before we apply it.

Helmut

Attachment: testcase.sh
Description: Bourne shell script

TESTS= \
        unpgzip-confgzip-unpzutils-confzutils-rmzutils \
        unpgzip-confgzip-unpzutils-rmzutils \
        unpgzip-unpzutils-confgzip-confzutils-rmzutils \
        unpgzip-unpzutils-confgzip-rmzutils \
        unpgzip-unpzutils-confzutils-confgzip-rmzutils \
        unpgzip-unpzutils-confzutils-rmzutils-confgzip \
        unpgzip-unpzutils-rmzutils-confgzip \
        unpzutils-unpgzip-confgzip-confzutils-rmzutils \
        unpzutils-unpgzip-confzutils-confgzip-rmzutils \
        unpzutils-unpgzip-rmzutils-confgzip \
        unpzutils-confzutils-unpgzip-confgzip-rmzutils \
        unpzutils-confzutils-rmzutils \
        unpzutils-rmzutils-unpgzip-confgzip \
        withzutils-deinstzutils-unpgzip-rmzutils-confgzip \
        
withzutils-deinstzutils-unpgzip-rmzutils-unpzutils-confgzip-confzutils-rmzutils 
\
        
withzutils-deinstzutils-unpgzip-rmzutils-unpzutils-confzutils-confgzip-rmzutils 
\
        withzutils-unpzutils-unpgzip-confgzip-confzutils-rmzutils \
        withzutils-unpzutils-unpgzip-confzutils-confgzip-rmzutils \
        withzutils-unpzutils-unpgzip-rmzutils-confgzip \
        withzutils-unpzutils-confzutils-unpgzip-confgzip-rmzutils \


all: $(foreach t,$(TESTS),testout/$(t))

testout/%:
        ./testcase.sh "$*" >"$@" 2>&1; echo $$? >> "$@"
diff --git a/debian/control b/debian/control
index 0020d21..31a2a1a 100644
--- a/debian/control
+++ b/debian/control
@@ -18,11 +18,6 @@ Architecture: any
 Depends:
  ${misc:Depends},
  ${shlibs:Depends},
-Breaks:
-# We must ensure that gzip is upgraded before zutils.postinst runs. As it is
-# essential, Breaks is sufficient here and the janitor may propose dropping
-# this relation eventually.
- gzip (<< 1.12-1.1~),
 Suggests:
  bzip2,
  lzip,
diff --git a/debian/zutils.preinst b/debian/zutils.preinst
index 0b242e6..3512868 100755
--- a/debian/zutils.preinst
+++ b/debian/zutils.preinst
@@ -4,14 +4,45 @@ set -e
 
 # DEP17 M18: Duplicate diversion in aliased location /bin.
 
+die() {
+       printf '%s, cannot proceed\n' "$*" 1>&2
+       exit 1
+}
+
 case "${1}" in
        install)
+               # Situations considered:
+               #  * bookworm or earlier gzip is fully installed.
+               #    -> No diversions, /bin/${FILE}
+               #  * trixie or later gzip is unpacked.
+               #    -> No diversions, /usr/bin/${FILE}
+               #  * trixie or later gzip has been unpacked while bookworm's
+               #    zutils was installed, but zutils has since been removed.
+               #    -> /usr/bin/FILE -> /usr/bin/FILE.usr-is-merged
+               #  * trixie or later gzip is fully installed.
+               #    -> No diversions, /usr/bin/${FILE}
+               #
+               # We cannot run between gzip.preinst and gzip unpack.
+               GZIP_VERSION=$(dpkg-query -f '${Version}' -W gzip)
+               GZIP_PREFIX=/usr
+               dpkg --compare-versions "$GZIP_VERSION" lt 1.12-1.1~ && 
GZIP_PREFIX=
                for FILE in zcat zcmp zdiff zegrep zfgrep zgrep
                do
-                       # We may move $FILE to $FILE.gzip when we expected 
$FILE.gzip.usr-is-merged here.
-                       # This is ok, because gzip will be upgraded and 
overwrite $FILE.gzip.
-                       dpkg-divert --package zutils --quiet --add --rename 
--divert "/usr/bin/${FILE}.gzip" "/usr/bin/${FILE}"
-                       dpkg-divert --package zutils --quiet --add --rename 
--divert "/bin/${FILE}.gzip.usr-is-merged" "/bin/${FILE}"
+                       TRUENAME=$(dpkg-divert --truename "/usr/bin/${FILE}")
+                       if [ "${TRUENAME}" = "/usr/bin/${FILE}.usr-is-merged" ]
+                       then
+                               dpkg-divert --package zutils --quiet --remove 
--no-rename --divert "${TRUENAME}" "/usr/bin/${FILE}"
+                       elif [ "${TRUENAME}" != "/usr/bin/${FILE}" ]
+                       then
+                               die "unexpected diversion of /usr/bin/${FILE} 
to ${TRUENAME}"
+                       fi
+                       # We cannot --rename, because it'll rename any existing
+                       # file without checking whether the file is owned or
+                       # not. Correctly compute the required rename depending
+                       # on the gzip version.
+                       dpkg-divert --package zutils --quiet --add --no-rename 
--divert "/usr/bin/${FILE}.gzip" "/usr/bin/${FILE}"
+                       dpkg-divert --package zutils --quiet --add --no-rename 
--divert "/bin/${FILE}.gzip.usr-is-merged" "/bin/${FILE}"
+                       mv "${DPKG_ROOT:-}${TRUENAME}" 
"${DPKG_ROOT:-}${GZIP_PREFIX}/bin/${FILE}.gzip${GZIP_PREFIX:+.usr-is-merged}"
                        dpkg-divert --package zutils --quiet --add --rename 
--divert /usr/share/man/man1/${FILE}.gzip.1.gz /usr/share/man/man1/${FILE}.1.gz
                done
                ;;
@@ -23,12 +54,23 @@ case "${1}" in
 
                        if [ "${TRUENAME}" = "/usr/bin/${FILE}.usr-is-merged" ]
                        then
-                               # gzip.preinst duplicated the diversion for us
+
+                               # This branch should be dead code. The
+                               # diversion indicates that trixie's gzip has
+                               # been unpacked, but trixie's gzip also
+                               # conflicts with zutils (<< trixie), so either
+                               # we're not installing or we are upgrading from
+                               # trixie.
+                               if ! [ -e "${DPKG_ROOT}${TRUENAME}" ]
+                               then
+                                       die "diverted file ${TRUENAME} does not 
exist"
+                               fi
                                dpkg-divert --package zutils --quiet --remove 
--no-rename --divert "/usr/bin/${FILE}.usr-is-merged" "/usr/bin/${FILE}"
                                dpkg-divert --package zutils --quiet --remove 
--no-rename "/bin/${FILE}"
                                dpkg-divert --package zutils --quiet --add 
--no-rename --divert "/usr/bin/${FILE}.gzip" "/usr/bin/${FILE}"
                                dpkg-divert --package zutils --quiet --add 
--no-rename --divert "/bin/${FILE}.gzip.usr-is-merged" "/bin/${FILE}"
-                       elif [ "${TRUENAME}" != "/usr/bin/${FILE}.gzip" ]
+                               mv "${DPKG_ROOT}${TRUENAME}" 
"${DPKG_ROOT}/usr/bin/${FILE}.gzip"
+                       elif [ "${TRUENAME}" = "/usr/bin/${FILE}" ]
                        then
                                dpkg-divert --package zutils --quiet --add 
--no-rename --divert "/usr/bin/${FILE}.gzip" "/usr/bin/${FILE}"
 
@@ -43,6 +85,9 @@ case "${1}" in
                                                mv "${DPKG_ROOT}${TRUENAME}" 
"${DPKG_ROOT}/bin/${FILE}.gzip.usr-is-merged"
                                        fi
                                fi
+                       elif [ "${TRUENAME}" != "/usr/bin/${FILE}.gzip" ]
+                       then
+                               die "unexpected diversion of /usr/bin/${FILE} 
to ${TRUENAME}"
                        fi
                done
                ;;

Reply via email to