--- pym/output.py (revision 6794) +++ pym/output.py (revision 6795) @@ -127,6 +127,7 @@ # Portage functions codes["INFORM"] = codes["darkgreen"] codes["UNMERGE_WARN"] = codes["red"] +codes["SECURITY_WARN"] = codes["red"] codes["MERGE_LIST_PROGRESS"] = codes["yellow"] def parse_color_map(): --- pym/portage.py (revision 6794) +++ pym/portage.py (revision 6795) @@ -7023,6 +7023,17 @@ before and after this method. """ + # When new_contents is supplied, the security check has already been + # done for this slot, so it shouldn't be repeated until the next + # replacement or unmerge operation. + if new_contents is None: + slot = self.vartree.dbapi.aux_get(self.mycpv, ["SLOT"])[0] + slot_matches = self.vartree.dbapi.match( + "%s:%s" % (dep_getkey(self.mycpv), slot)) + retval = self._security_check(slot_matches) + if retval: + return retval + contents = self.getcontents() # Now, don't assume that the name of the ebuild is the same as the # name of the dir; the package may have been moved. @@ -7236,12 +7247,9 @@ writemsg_stdout("--- !md5 %s %s\n" % ("obj", obj)) continue try: - if statobj.st_mode & (stat.S_ISUID | stat.S_ISGID): - # Always blind chmod 0 before unlinking to avoid race conditions. - os.chmod(obj, 0000) - if statobj.st_nlink > 1: - writemsg("setXid: "+str(statobj.st_nlink-1)+ \ - " hardlinks to '%s'\n" % obj) + # Remove permissions to ensure that any hardlinks to + # suid/sgid files are rendered harmless. + os.chmod(obj, 0) os.unlink(obj) except (OSError,IOError),e: pass @@ -7305,6 +7313,48 @@ return False + def _security_check(self, slot_matches): + if not slot_matches: + return 0 + file_paths = set() + for cpv in slot_matches: + file_paths.update(dblink(self.cat, catsplit(cpv)[1], + self.vartree.root, self.settings, + vartree=self.vartree).getcontents()) + inode_map = {} + for path in file_paths: + try: + s = os.lstat(path) + except OSError, e: + if e.errno != errno.ENOENT: + raise + del e + continue + if stat.S_ISREG(s.st_mode) and \ + s.st_nlink > 1 and \ + s.st_mode & (stat.S_ISUID | stat.S_ISGID): + k = (s.st_dev, s.st_ino) + inode_map.setdefault(k, []).append((path, s)) + suspicious_hardlinks = [] + for path_list in inode_map.itervalues(): + path, s = path_list[0] + if len(path_list) == s.st_nlink: + # All hardlinks seem to be owned by this package. + continue + suspicious_hardlinks.append(path_list) + if not suspicious_hardlinks: + return 0 + from output import colorize + prefix = colorize("SECURITY_WARN", "*") + " WARNING: " + writemsg(prefix + "suid/sgid file(s) " + \ + "with suspicious hardlink(s):\n", noiselevel=-1) + for path_list in suspicious_hardlinks: + for path, s in path_list: + writemsg(prefix + " '%s'\n" % path, noiselevel=-1) + writemsg(prefix + "See the Gentoo Security Handbook " + \ + "guide for advice on how to proceed.\n", noiselevel=-1) + return 1 + def treewalk(self, srcroot, destroot, inforoot, myebuild, cleanup=0, mydbapi=None, prev_mtimes=None): """ @@ -7352,6 +7402,10 @@ slot_matches = self.vartree.dbapi.match( "%s:%s" % (self.mysplit[0], self.settings["SLOT"])) + retval = self._security_check(slot_matches) + if retval: + return retval + if slot_matches: # Used by self.isprotected(). max_cpv = None