diff -Nru unattended-upgrades-1.1ubuntu1/debian/changelog unattended-upgrades-1.1ubuntu1.18.04.1/debian/changelog --- unattended-upgrades-1.1ubuntu1/debian/changelog 2018-04-17 14:53:30.000000000 +0000 +++ unattended-upgrades-1.1ubuntu1.18.04.1/debian/changelog 2018-06-06 23:30:55.000000000 +0000 @@ -1,3 +1,25 @@ +unattended-upgrades (1.1ubuntu1.18.04.1) bionic; urgency=medium + + [ Michael Vogt ] + * unattended-upgrades: fix Unlocked context manager. (LP: #1602536) + The Unlocked context manager did correctly unlock but did not + reacquire the lock which means that in minimal-upgrade step + mode it is possible to run apt code without a lock. If something + else (like landscape, apt, synaptic, packagekit) locks the cache + in the meantime this will work and u-u will get dpkg errors + because dpkg will not be able to perform its operations. It is + less of an issue in non-minimal mode, but even then the auto-remove + step may fail in this way. + + [ Balint Reczey ] + * Fix adjusting candidates (LP: #1775292) + * Relock apt lock before reopening the cache (LP: #1602536) + * Fix crashing while adjusting candidates and save candidates to adjust only + in first sweep run, not emptying the set later + (Closes: #901258) (LP: #1775307) + + -- Balint Reczey Wed, 06 Jun 2018 16:30:55 -0700 + unattended-upgrades (1.1ubuntu1) bionic; urgency=medium * Merge from Debian unstable (LP: #1764797) diff -Nru unattended-upgrades-1.1ubuntu1/test/unattended_upgrade.py unattended-upgrades-1.1ubuntu1.18.04.1/test/unattended_upgrade.py --- unattended-upgrades-1.1ubuntu1/test/unattended_upgrade.py 2018-04-17 14:53:30.000000000 +0000 +++ unattended-upgrades-1.1ubuntu1.18.04.1/test/unattended_upgrade.py 2018-06-06 23:30:55.000000000 +0000 @@ -117,7 +117,7 @@ def __init__(self, rootdir, allowed_origins): # type: (str, List[str]) -> None - self._cached_candidate_pkgnames = set() # type: AbstractSet[str] + self._cached_candidate_pkgnames = None # type: AbstractSet[str] self.allowed_origins = allowed_origins apt.Cache.__init__(self, rootdir=rootdir) @@ -157,54 +157,61 @@ # ensure we update the candidate versions self.adjust_candidates() - def adjust_candidates(self): - for pkgname, candidate in self._get_candidates_to_adjust(): - self[pkgname].candidate = candidate - - def _get_candidates_to_adjust(self): - # type: () -> Dict[str, apt.Version] - """ Get candidate versions needed to be adjusted to match highest - allowed origin + def adjust_candidate(self, pkg): + # type: (apt.Package) -> bool + """ Adjust origin and return True if adjustment took place - This lets adjusting the origin even if the candidate has a higher - version. This is needed when e.g. a package is available in + This is needed when e.g. a package is available in the security pocket but there is also a package in the updates pocket with a higher version number """ - candidates = {} + new_cand = None + for ver in pkg.versions: + # ignore versions that the user marked with priority < 100 + # (and ensure we have a python-apt that supports this) + if (hasattr(ver, "policy_priority") and ver.policy_priority < 100): + logging.debug("ignoring ver %s with priority < 0" % ver) + continue + if is_allowed_origin(ver, self.allowed_origins): + # leave as soon as we have the highest new candidate + new_cand = ver + break + if new_cand and new_cand != pkg.candidate: + logging.debug("adjusting candidate version: %s" % new_cand) + pkg.candidate = new_cand + return True + return False + + def adjust_candidates(self): + # tyoe: () -> None + """ Adjust all package candidates when needed saving cached package + names to self._cached_candidate_pkgnames + """ if self._cached_candidate_pkgnames is None: - pkg_iter = iter(self.keys()) + # first run, collect package names + self._cached_candidate_pkgnames = set() + for pkg in self: + # important! this avoids downgrades below + if pkg.is_installed and not pkg.is_upgradable: + continue + # check if the candidate is already pointing to a allowed + # origin and if so, do not mess with it + if is_allowed_origin(pkg.candidate, self.allowed_origins): + continue + # check if we have a version in a allowed origin that is + # not the candidate + if self.adjust_candidate(pkg): + # ... and save it for next runs + self._cached_candidate_pkgnames.add(pkg.name) else: - pkg_iter = iter( - [self[name] for name in self._cached_candidate_pkgnames]) - for pkg in pkg_iter: - # important! this avoids downgrades below - if pkg.is_installed and not pkg.is_upgradable: - continue - # check if the candidate is already pointing to a allowed - # origin and if so, do not mess with it - if is_allowed_origin(pkg.candidate, self.allowed_origins): - continue - # check if we have a version in a allowed origin that is - # not the candidate - new_cand = None - for ver in pkg.versions: - # ignore versions that the user marked with priority < 100 - # (and ensure we have a python-apt that supports this) - if (hasattr(ver, "policy_priority") and - ver.policy_priority < 100): - logging.debug("ignoring ver %s with priority < 0" % ver) + # packages to be adjusted are already collected + for pkgname in self._cached_candidate_pkgnames: + pkg = self[pkgname] + # important! this avoids downgrades and also skips packages + # installed since previous adjustment runs + if pkg.is_installed and not pkg.is_upgradable: continue - if is_allowed_origin(ver, self.allowed_origins): - # leave as soon as we have the highest new candidate - new_cand = ver - break - if new_cand and new_cand != pkg.candidate: - logging.debug("adjusting candidate version: %s" % new_cand) - candidates[pkg.name] = new_cand - self._cached_candidate_pkgnames = set( - [name for name in candidates.keys()]) - return candidates + self.adjust_candidate(pkg) class LogInstallProgress(apt.progress.base.InstallProgress): @@ -350,7 +357,7 @@ def __exit__(self, exc_type, exc_value, exc_tb): # type: (object, object, object) -> None try: - apt_pkg.pkgsystem_unlock() + apt_pkg.pkgsystem_lock() except Exception: pass @@ -508,14 +515,15 @@ if iprogress is None: iprogress = LogInstallProgress(logfile_dpkg, verbose) - with Unlocked(): - try: + try: + with Unlocked(): res = cache.commit(install_progress=iprogress) - cache.open() - except SystemError as e: - error = e - if verbose: - logging.exception("Exception happened during upgrade.") + cache.open() + except SystemError as e: + error = e + if verbose: + logging.exception("Exception happened during upgrade.") + return res, error diff -Nru unattended-upgrades-1.1ubuntu1/unattended-upgrade unattended-upgrades-1.1ubuntu1.18.04.1/unattended-upgrade --- unattended-upgrades-1.1ubuntu1/unattended-upgrade 2018-04-17 14:53:30.000000000 +0000 +++ unattended-upgrades-1.1ubuntu1.18.04.1/unattended-upgrade 2018-06-06 23:30:55.000000000 +0000 @@ -117,7 +117,7 @@ def __init__(self, rootdir, allowed_origins): # type: (str, List[str]) -> None - self._cached_candidate_pkgnames = set() # type: AbstractSet[str] + self._cached_candidate_pkgnames = None # type: AbstractSet[str] self.allowed_origins = allowed_origins apt.Cache.__init__(self, rootdir=rootdir) @@ -157,54 +157,61 @@ # ensure we update the candidate versions self.adjust_candidates() - def adjust_candidates(self): - for pkgname, candidate in self._get_candidates_to_adjust(): - self[pkgname].candidate = candidate - - def _get_candidates_to_adjust(self): - # type: () -> Dict[str, apt.Version] - """ Get candidate versions needed to be adjusted to match highest - allowed origin + def adjust_candidate(self, pkg): + # type: (apt.Package) -> bool + """ Adjust origin and return True if adjustment took place - This lets adjusting the origin even if the candidate has a higher - version. This is needed when e.g. a package is available in + This is needed when e.g. a package is available in the security pocket but there is also a package in the updates pocket with a higher version number """ - candidates = {} + new_cand = None + for ver in pkg.versions: + # ignore versions that the user marked with priority < 100 + # (and ensure we have a python-apt that supports this) + if (hasattr(ver, "policy_priority") and ver.policy_priority < 100): + logging.debug("ignoring ver %s with priority < 0" % ver) + continue + if is_allowed_origin(ver, self.allowed_origins): + # leave as soon as we have the highest new candidate + new_cand = ver + break + if new_cand and new_cand != pkg.candidate: + logging.debug("adjusting candidate version: %s" % new_cand) + pkg.candidate = new_cand + return True + return False + + def adjust_candidates(self): + # tyoe: () -> None + """ Adjust all package candidates when needed saving cached package + names to self._cached_candidate_pkgnames + """ if self._cached_candidate_pkgnames is None: - pkg_iter = iter(self.keys()) + # first run, collect package names + self._cached_candidate_pkgnames = set() + for pkg in self: + # important! this avoids downgrades below + if pkg.is_installed and not pkg.is_upgradable: + continue + # check if the candidate is already pointing to a allowed + # origin and if so, do not mess with it + if is_allowed_origin(pkg.candidate, self.allowed_origins): + continue + # check if we have a version in a allowed origin that is + # not the candidate + if self.adjust_candidate(pkg): + # ... and save it for next runs + self._cached_candidate_pkgnames.add(pkg.name) else: - pkg_iter = iter( - [self[name] for name in self._cached_candidate_pkgnames]) - for pkg in pkg_iter: - # important! this avoids downgrades below - if pkg.is_installed and not pkg.is_upgradable: - continue - # check if the candidate is already pointing to a allowed - # origin and if so, do not mess with it - if is_allowed_origin(pkg.candidate, self.allowed_origins): - continue - # check if we have a version in a allowed origin that is - # not the candidate - new_cand = None - for ver in pkg.versions: - # ignore versions that the user marked with priority < 100 - # (and ensure we have a python-apt that supports this) - if (hasattr(ver, "policy_priority") and - ver.policy_priority < 100): - logging.debug("ignoring ver %s with priority < 0" % ver) + # packages to be adjusted are already collected + for pkgname in self._cached_candidate_pkgnames: + pkg = self[pkgname] + # important! this avoids downgrades and also skips packages + # installed since previous adjustment runs + if pkg.is_installed and not pkg.is_upgradable: continue - if is_allowed_origin(ver, self.allowed_origins): - # leave as soon as we have the highest new candidate - new_cand = ver - break - if new_cand and new_cand != pkg.candidate: - logging.debug("adjusting candidate version: %s" % new_cand) - candidates[pkg.name] = new_cand - self._cached_candidate_pkgnames = set( - [name for name in candidates.keys()]) - return candidates + self.adjust_candidate(pkg) class LogInstallProgress(apt.progress.base.InstallProgress): @@ -350,7 +357,7 @@ def __exit__(self, exc_type, exc_value, exc_tb): # type: (object, object, object) -> None try: - apt_pkg.pkgsystem_unlock() + apt_pkg.pkgsystem_lock() except Exception: pass @@ -508,14 +515,15 @@ if iprogress is None: iprogress = LogInstallProgress(logfile_dpkg, verbose) - with Unlocked(): - try: + try: + with Unlocked(): res = cache.commit(install_progress=iprogress) - cache.open() - except SystemError as e: - error = e - if verbose: - logging.exception("Exception happened during upgrade.") + cache.open() + except SystemError as e: + error = e + if verbose: + logging.exception("Exception happened during upgrade.") + return res, error