[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

booststrapping /usr-merged systems (was: Re: DEP 17: Improve support for directory aliasing in dpkg)



Hi,

This bootstrap aspect got me and I discussed this with a number of
people and did some research.

On Sun, May 07, 2023 at 12:51:21PM +0100, Luca Boccassi wrote:
> I don't think this is true? At least not in the broader sense: if you
> compile something on Debian, it will obviously get linked against
> libraries and dependencies as they are in Debian.
> Perhaps what you mean is that, given an entire separate sysroot-like
> tree, passing the appropriate compiler and linker flags and
> environment variables, you can use the local compiler we ship to build
> 'foreign' programs. That is true, but again it requires to set up the
> environment appropriately, including linker flags. And the caller
> needs to ensure the environment, including linker flags, is
> appropriate for the target environment (I guess 'host' environment, in
> GNU parlance). Therefore, I don't think it would be unreasonable to
> require that if the target environment is split-usr, then the caller
> also needs to specify an appropriate
> '-Wl,--dynamic-linker=/lib/ld-whatever' option.

Given the feedback, I am convinced that changing PT_INTERP is a stupid
idea regardless of whether it is technically feasible. There must be a
better way. Let's step back a bit.

The underlying problem here is performing the initial filesystem
bootstrap. The semantics of this are a bit vague as they are not spelled
out in policy, so we will have to derive them from implementations.

I think the major players are (in descending popularity):
 * debootstrap
 * mmdebstrap
 * cdebootstrap
 * multistrap

multistrap predates mmdebstrap and when there was no mmdebstrap, I used
it a lot. When attempting to test it, I totally couldn't convince it to
bootstrap from an unsigned or locally signed repository.  The patch in
#908451 didn't cut it. I also note that it creates a /lib64 -> /lib
symbolic link which feels quite incompatible with merged-/usr.  For
these reasons, I am dropping multistrap from the tools under
consideration and recommend removing it from the archive. If you happen
to use multistrap, now would be a good moment to tell me. Personally,
all of my use cases of multistrap have been converted to mmdebstrap and
that made a lot of things simpler.

cdebootstrap vaguely works though unsigned operation seems dysfunctional
as it runs apt-get update during cdebootstrap-helper-apt.postinst and
that fails. I happen to not have figured out why and treat this failure
as a success.

So the most popular implementations quite evidently are debootstrap and
mmdebstrap and both "just work". I note though that they work quite
differently:
 * debootstrap (depending on flags including --variant) pre-merges its
   chroot while mmdebstrap relies on packages doing it.

   I think that the question whether a distribution is merged is a
   property of the distribution and not the bootstrap tool, so I
   strongly recommend following mmdebstrap's view on this. The
   debootstrap way means that we have to include patches for every
   derivative, which is a process that does not scale well.

 * mmdebstrap operates in two phases. It first unpacks and configures a
   rather minimal set of packages and then proceeds to adding packages
   passed to --include in a second phase once essential is fully
   configured while debootstrap immediately unpacks everything.

   I think the debootstrap approach is slightly worse here, because it
   means that preinst scripts of non-essential packages cannot rely on
   essential packages having been configured.

In any case, we have to deal with both behaviours.

After this little excursion into bootstrap technology, let's go back to
the /usr-merge and its effects.

I think at this point, we have quite universal consensus about the goal
of moving files to their canonical location (i.e. from / to /usr) as a
solution to the aliasing problems while we do not have consensus on
precisely how to do this (i.e. with changing dpkg or without). If you
believe that this is not consensus, please speak up.

So in a distant future our packages will not contain any files in /bin
or /lib. In particular, this affects /bin/sh and the dynamic loader,
both of which are required to run maintainer scripts, which are
currently required for creating the symbolic links. Boom.

Solutions have been proposed to this and I think they all fall into one
of the following four categories.

 1. Don't move. We just keep those files that require a particular
    location (such as /bin/sh or the dynamic loader) in their
    non-canonical location. As such, maintainer scripts will be able to
    run and perform the conversion to symbolic links afterwards.

 2. Move and ship links. Since we unpack all essential data.tar before
    running the first maintainer script, having one package contain the
    compatibility symlinks is enough to fix the problem.

 3. Move and avoid using non-canonical locations. This is the approach
    where we write maintainer scripts as #!/usr/bin/sh and considered
    changing PT_INTERP.

 4. Change the bootstrap protocol. In essence, this has been attempted
    in debootstrap by creating these symlinks prior to unpack, but no
    consensus has evolved around this approach yet. The category is
    wider though and generally requires changes to all bootstrapping
    tools.

To see that it's just these four we can watch an imaginary bootstrap
process on an imaginary distribution. If any package contains the
symlinks, we're in category 2.  Otherwise, we see whether these symlinks
exist prior to running the first maintainer script. If they do, we're in
category 4. And finally, we see whether any files are left in aliased
locations (according to the dpkg database). If there are, we're category
1 and otherwise category 3.

For instance, Luca proposed changing PT_INTERP in the toolchain, which
is category 3 here. I proposed a minor adaption where we have debhelper
change it post build using patchelf, which also amounts to category 3.
I've started with these to get started with classifying and think we can
disregard both.

For completeness sake, there is one more entry in category 3: We can run
the dynamic loader from its canonical location explicitly, so we'd
modify maintainer scripts to start with:

    #!/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/bin/sh

This would only affect preinst scripts participating in bootstrap and
we'd not bother with changing PT_INTERP in the toolchain nor in
packages. Unfortunately, this completely breaks the DPKG_ROOT work and
with that, I see no viable entries in category 3 for moving forward.

Moving on to category 4 feels rather obvious, especially because work
has been done there in debootstrap. The approach in debootstrap however
is one that I see as a dead end, because it causes us to maintain this
code multiple times. It's the number of derivatives times the number of
bootstrap tools and that doesn't scale.

Category 4 is wider though and we also have other prior art at
https://wiki.debian.org/Teams/Dpkg/Spec/InstallBootstrap. In particular,
making architecture bootstrap become chrootless would likely be a
generic solution to this and other problems.  However, any change needs
to propagate to a stable release in all bootstrapping tools.  Therefore
we cannot reasonably finish the transition before forky.  This makes
category 4 rather unattractive in a short term, but still worth pursuing
in a long term.

Having ruled out categories 3 and 4 maybe category 2 would be good?  We
could just ship those symlinks in base-files and be done, right?
Unfortunately, we pass -k to tar in debootstrap, so when it extracts
base-files and tries to unpack the bin -> usr/bin symlink, it sees that
oh no, there already is a symlink at bin (as debootstrap placed it
there) and thus fails. So in order to make this work, we also have to
modify debootstrap (and thus are in a combination of category 3 and 4).

Conversely, if we unpack anything else that happens to ship
/bin/something before base-files, then mmdebstrap will fail to unpack
base-files. So we can only add this link to base-files after all other
(essential) packages have moved everything out of bin. That happens to
include /bin/sh. A possible solution to this is doing all of this in the
same dinstall. Then mmdebstrap works before and works afterwards.

Unfortunately, we're not yet done here. When (for instance) dash is the
last package to move /bin/sh and such to /usr and you upgrade dash, dpkg
notices that no package owns /bin anymore. Thus it helpfully deletes
/bin (the symlink).  You're not happy when this happens. We remember the
silver bullet: diversions! So dash.preinst could dpkg-divert --no-rename
--divert /bin.usrmerged -add /bin and dash.postinst could revert that.
Unfortunately, such a diversion affects every package but dash and we
want it exactly the other way round. So what we could do is pass
--package dash-usrmerged (which must not exist). Then it'll actually
keep /bin safe. Unfortunately, we don't know whether dash or bash will
be the last package owning /bin, so both of them need this diversion and
this is a conflict.

And as if that wasn't enough, we also run into issues around hard links.
As dpkg unpacks a canonicalized gzip, it notices that /bin/gunzip (which
is scheduled for deletion) has the same inode as the new
/usr/bin/uncompress (because gunzip and uncompress are hard linked). As
far as I can see, all we get here is a warning and both files survive
the unpack. It is not clear to me whether this happens by chance or
reliably.

    dpkg: warning: old file '/bin/uncompress' is the same as several new files! (both '/usr/bin/gunzip' and '/usr/bin/uncompress')^
    dpkg: warning: old file '/bin/gunzip' is the same as several new files! (both '/usr/bin/gunzip' and '/usr/bin/uncompress')

Given what I've seen, I'm fairly convinced that I haven't reached the
bottom of it and I'm ready to conclude that this approach is fragile - a
property that is most unwelcome when we deal with the essential set.

So what's left is category 1. I looked into what the minimum set of
files to be retained could be. To do that end, I moved everything and
then reverted as much as was needed to make bootstrapping work.
 * /lib64/ld-linux-x86-64.so.2 (hopefully obvious)
 * /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 (link target)
 * /bin/sh (hopefully obvious)
 * /bin/dash (link target)
 * /bin/bash (usrmerge runs ldd, which is a #!/bin/bash script)
 * /bin/more (update-alternatives doesn't like its absence)
 * /bin/cp (unless usrmerge stops hard coding its path)

So in principle, we may be able to pull this off if we keep bash,
dash, libc6, and util-linux in their original location. In this
scenario, we're also unable to remove usrmerge from the essential set.
The major benefit of category 1 it allows us to move to any other
category at a later time and it removes aliasing effects from all but a
small set of packages.

In case you want to reproduce my analysis, I've attached a
test-usrmergebootstrap.sh script. It attempts the following strategies:
 * "earlylink": add links to base-files first without converting other
   packages
 * "movefull": move all files to their canonical locations and add links
   to base-files
 * "movemost": move most files except those mentioned earlier to their
   canonical locations without adding links to base-files
These strategies are tested in the following use cases:
 * "cdebootstrap": bootstrap using cdebootstrap
 * "debootstrap": bootstrap using debootstrap
 * "debootstrap-buildd": bootstrap using debootstrap --variant=buildd
   --merged-usr
 * "mmdebstrap": bootstrap using mmdebstrap
 * "upgrade": perform a regular bootstrap and upgrade packages to the
   modified ones

Broken combinations
 * *strap-earlylink: unpack error during base-files
 * debootstrap*-movefull: unpack error during base-files
 * upgrade-movefull: /lib64 missing unless diverted for libc6

And now you probably expect me to write some kind of conclusion, but
really I think none of our options are attractive. In a short term I
strongly recommend not moving files around that are part of the
transitively essential set, because things can (and do) go wrong in so
many surprising ways. The other major takeaway is that a significant
chunk of the problems mentioned in this mail cannot be fixed by
modifying dpkg only.

I sincerely hope that everyone will now point out why this analysis is
incomplete and misses out on covering strategies. I'd love to be wrong
about the dark picture I've drawn here.

Helmut

Attachment: test-usrmergebootstrap.sh
Description: Bourne shell script


Reply to: