From 4bb08136f073024c5d31dceb1618b6f4e7246369 Mon Sep 17 00:00:00 2001 From: Zac Medico Date: Thu, 28 Jul 2011 04:29:25 -0700 Subject: [PATCH] emerge: protect symlinks to directories sometimes Before, it was possible to unmerge a symlink to a directory, such that files installed via the path of the symlink could become inaccessible via that path (and also making it impossible to unmerge them via that path). Now, the symlink will only be unmerged if the directory that it points to only contains regular files which are all being unmerged. In any other case, the symlink will be preserved and an eerror log message will record the event. This will give the user an opportunity to take further action if they deem it necessary, and such symlink preservation will not be silent as it was reported in bug #326685, comment #3. --- pym/portage/dbapi/vartree.py | 90 +++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/pym/portage/dbapi/vartree.py b/pym/portage/dbapi/vartree.py index 47f5eb2e1..6bb68d344 100644 --- a/pym/portage/dbapi/vartree.py +++ b/pym/portage/dbapi/vartree.py @@ -70,6 +70,7 @@ import shutil import stat import sys import tempfile +import textwrap import time import warnings @@ -1908,6 +1909,7 @@ class dblink(object): cfgfiledict = grabdict(self.vartree.dbapi._conf_mem_file) stale_confmem = [] + protected_symlinks = [] unmerge_orphans = "unmerge-orphans" in self.settings.features calc_prelink = "prelink-checksums" in self.settings.features @@ -2027,9 +2029,34 @@ class dblink(object): if dblnk.isowner(relative_path): is_owned = True break - if is_owned: + + if is_owned and \ + (islink and statobj and stat.S_ISDIR(statobj.st_mode)): # A new instance of this package claims the file, so - # don't unmerge it. + # don't unmerge it. If the file is symlink to a + # directory and the unmerging package installed it as + # a symlink, but the new owner has it listed as a + # directory, then we'll produce a warning since the + # symlink is a sort of orphan in this case (see + # bug #326685). + symlink_orphan = False + for dblnk in others_in_slot: + parent_contents_key = \ + dblnk._match_contents(relative_path) + if not parent_contents_key: + continue + if not parent_contents_key.startswith( + real_root): + continue + if dblnk.getcontents()[ + parent_contents_key][0] == "dir": + symlink_orphan = True + break + + if symlink_orphan: + protected_symlinks.append(relative_path) + + if is_owned: show_unmerge("---", unmerge_desc["replaced"], file_type, obj) continue elif relative_path in cfgfiledict: @@ -2076,6 +2103,52 @@ class dblink(object): if not islink: show_unmerge("---", unmerge_desc["!sym"], file_type, obj) continue + + # If this symlink points to a direcory then we don't want + # to unmerge it if there are any other packages that + # installed files into the directory via this symlink + # (see bug #326685). + # TODO: Resolving a symlink to a directory will require + # simulation if $ROOT != / and the link is not relative. + if islink and statobj and stat.S_ISDIR(statobj.st_mode) \ + and obj.startswith(real_root): + + relative_path = obj[real_root_len:] + try: + target_dir_contents = os.listdir(obj) + except OSError: + pass + else: + if target_dir_contents: + # If all the children are regular files owned + # by this package, then the symlink should be + # safe to unmerge. + all_owned = True + for child in target_dir_contents: + child = os.path.join(relative_path, child) + if not self.isowner(child): + all_owned = False + break + try: + child_lstat = os.lstat(os.path.join( + real_root, child.lstrip(os.sep))) + except OSError: + continue + + if not stat.S_ISREG(child_lstat.st_mode): + # Nested symlinks or directories make + # the issue very complex, so just + # preserve the symlink in order to be + # on the safe side. + all_owned = False + break + + if not all_owned: + protected_symlinks.append(relative_path) + show_unmerge("---", unmerge_desc["!empty"], + file_type, obj) + continue + # Go ahead and unlink symlinks to directories here when # they're actually recorded as symlinks in the contents. # Normally, symlinks such as /lib -> lib64 are not recorded @@ -2152,6 +2225,19 @@ class dblink(object): show_unmerge("---", unmerge_desc["!empty"], "dir", obj) del e + if protected_symlinks: + msg = "One or more symlinks to directories have been " + \ + "preserved in order to ensure that files installed " + \ + "via these symlinks remain accessible:" + lines = textwrap.wrap(msg, 72) + lines.append("") + protected_symlinks.reverse() + for f in protected_symlinks: + lines.append("\t%s" % (os.path.join(real_root, + f.lstrip(os.sep)))) + lines.append("") + self._elog("eerror", "postrm", lines) + # Remove stale entries from config memory. if stale_confmem: for filename in stale_confmem: -- 2.26.2