Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package rpmlint for openSUSE:Factory checked in at 2026-02-14 21:36:35 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rpmlint (Old) and /work/SRC/openSUSE:Factory/.rpmlint.new.1977 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rpmlint" Sat Feb 14 21:36:35 2026 rev:527 rq:1332753 version:2.9.0+git20260211.72e34881 Changes: -------- --- /work/SRC/openSUSE:Factory/rpmlint/rpmlint.changes 2026-02-11 18:48:24.140255657 +0100 +++ /work/SRC/openSUSE:Factory/.rpmlint.new.1977/rpmlint.changes 2026-02-14 21:37:17.748593065 +0100 @@ -1,0 +2,8 @@ +Thu Feb 12 17:42:03 UTC 2026 - Filippo Bonazzi <[email protected]> + +- Update to version 2.9.0+git20260211.72e34881: + * whitelistings: add gdm-50 D-Bus and Polkit changes (bsc#1258025) + * dbus-services: remove no longer necessary gdm.conf compatibility entry + * SUIDPermissionsCheck: support permissions package coupling + +------------------------------------------------------------------- Old: ---- rpmlint-2.9.0+git20260211.ecf25fcf.tar.xz New: ---- rpmlint-2.9.0+git20260211.72e34881.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rpmlint.spec ++++++ --- /var/tmp/diff_new_pack.g6UcA6/_old 2026-02-14 21:37:19.024645664 +0100 +++ /var/tmp/diff_new_pack.g6UcA6/_new 2026-02-14 21:37:19.028645829 +0100 @@ -23,7 +23,7 @@ %define name_suffix -%{flavor} %endif Name: rpmlint%{name_suffix} -Version: 2.9.0+git20260211.ecf25fcf +Version: 2.9.0+git20260211.72e34881 Release: 0 Summary: RPM file correctness checker License: GPL-2.0-or-later ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.g6UcA6/_old 2026-02-14 21:37:19.096648631 +0100 +++ /var/tmp/diff_new_pack.g6UcA6/_new 2026-02-14 21:37:19.096648631 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/rpm-software-management/rpmlint.git</param> - <param name="changesrevision">ecf25fcf56dcdf75e69af91282446880429f65c3</param></service></servicedata> + <param name="changesrevision">72e3488149426f36c8470672343d835b091ba87e</param></service></servicedata> (No newline at EOF) ++++++ rpmlint-2.9.0+git20260211.ecf25fcf.tar.xz -> rpmlint-2.9.0+git20260211.72e34881.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-2.9.0+git20260211.ecf25fcf/configs/openSUSE/dbus-services.toml new/rpmlint-2.9.0+git20260211.72e34881/configs/openSUSE/dbus-services.toml --- old/rpmlint-2.9.0+git20260211.ecf25fcf/configs/openSUSE/dbus-services.toml 2026-02-11 09:10:05.000000000 +0100 +++ new/rpmlint-2.9.0+git20260211.72e34881/configs/openSUSE/dbus-services.toml 2026-02-11 17:27:36.000000000 +0100 @@ -71,22 +71,22 @@ package = "gdm" type = "dbus" note = "D-Bus interface for managing GDM sessions" -bugs = ["bsc#1204052", "bsc#1218922", "bsc#1230466"] +bugs = ["bsc#1204052", "bsc#1218922", "bsc#1230466", "bsc#1248881"] [[FileDigestGroup.digests]] path = "/usr/share/dbus-1/system.d/gdm.conf" digester = "xml" -hash = "6c74cd8824a587ccd281886c655718dfecd1da530bb823e6625be4a22c5a09e6" +hash = "cf4cb93af82aacc9b4a0fc600c602af5089e001df10282a35287b6f27199f7ea" -# TEMPORARY ENTRY. Remove as soon as possible (bsc#1230466). +# TODO: merge with entry above once GDM 50 enters Factory [[FileDigestGroup]] package = "gdm" -type = "dbus" note = "D-Bus interface for managing GDM sessions" -bugs = ["bsc#1204052", "bsc#1218922", "bsc#1230466", "bsc#1248881"] +bug = "bsc#1258025" +type = "dbus" [[FileDigestGroup.digests]] path = "/usr/share/dbus-1/system.d/gdm.conf" digester = "xml" -hash = "cf4cb93af82aacc9b4a0fc600c602af5089e001df10282a35287b6f27199f7ea" +hash = "a46c9af0b5702e1d4cfa643b8f74ca878c3aa9cdd2d6e42d53ba160d64fc8e20" [[FileDigestGroup]] package = "udisks2" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-2.9.0+git20260211.ecf25fcf/configs/openSUSE/polkit-rules-whitelist.toml new/rpmlint-2.9.0+git20260211.72e34881/configs/openSUSE/polkit-rules-whitelist.toml --- old/rpmlint-2.9.0+git20260211.ecf25fcf/configs/openSUSE/polkit-rules-whitelist.toml 2026-02-11 09:10:05.000000000 +0100 +++ new/rpmlint-2.9.0+git20260211.72e34881/configs/openSUSE/polkit-rules-whitelist.toml 2026-02-11 17:27:36.000000000 +0100 @@ -228,6 +228,17 @@ digester = "default" hash = "38aaac33cd24fca2db1bdf35389b0d52bc17741ae55f0de4ea35d16d5817cd24" +# TODO: merge with entry above once GDM 50 enters Factory +[[FileDigestGroup]] +package = "gdm" +note = "allows the display manager to add WiFi connections via NetworkManager, as well as creation of headless VNC displays" +bug = "bsc#1258025" +type = "polkit" +[[FileDigestGroup.digests]] +path = "/usr/share/polkit-1/rules.d/20-gdm.rules" +digester = "default" +hash = "134e91ed394511cda9e7aec31d95922cb4cd268314688ecd06fe4c11b1e9ae62" + [[FileDigestGroup]] packages = ["systemd", "systemd-mini"] note = "This is just an example file that will not be active by default" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-2.9.0+git20260211.ecf25fcf/rpmlint/checks/SUIDPermissionsCheck.py new/rpmlint-2.9.0+git20260211.72e34881/rpmlint/checks/SUIDPermissionsCheck.py --- old/rpmlint-2.9.0+git20260211.ecf25fcf/rpmlint/checks/SUIDPermissionsCheck.py 2026-02-11 09:10:05.000000000 +0100 +++ new/rpmlint-2.9.0+git20260211.72e34881/rpmlint/checks/SUIDPermissionsCheck.py 2026-02-11 17:27:36.000000000 +0100 @@ -1,5 +1,4 @@ import os -import re import stat import rpm @@ -10,12 +9,22 @@ class SUIDPermissionsCheck(AbstractCheck): + """Restrict installation of files with special privileges (set*id bits, + capabilities).""" + def __init__(self, config, output): super().__init__(config, output) + # maps normalized paths (without trailing slash) to a list of [PermissionsEntry] + # + # the list values are necessary since multiple entries can exist for + # the same path, tied to different packages (or tied to no package at + # all). self.perms = {} - self.var_handler = VariablesHandler(f'{SHARE_DIR}/variables.conf') + # parse the central permissions profiles: the static configuration and + # the secure profile. The latter is the reference profile decisions + # are based on in _verify_entry(). for fname in self._paths_to('permissions', 'permissions.secure'): if not os.path.exists(fname): continue @@ -26,75 +35,59 @@ parser = PermissionsParser(self.var_handler, path) self.perms.update(parser.entries) - def _check_restricted_mode(self, pkg, path, mode): + def _complain_restricted_mode(self, pkg, path, mode): msg = f'{path} is packaged with setuid/setgid bits (0{stat.S_IMODE(mode):o})' - if not stat.S_ISDIR(mode): - self.output.add_info('E', pkg, 'permissions-file-setuid-bit', msg) - else: - self.output.add_info('E', pkg, 'permissions-directory-setuid-bit', msg) - - def _verify_entry(self, pkg, path, mode, owner): - entry = self.perms[path] + diag = 'permissions-directory-setuid-bit' if stat.S_ISDIR(mode) else 'permissions-file-setuid-bit' + self.output.add_info('E', pkg, diag, msg) + def _verify_entry(self, entry, pkg, path, rpm_mode, rpm_owner): + """Complains about disagreements between the package metadata and the + permissions profile settings. We also require the RPM permissions to + match the reference permissions profile (secure).""" is_listed_as_dir = entry.path.endswith('/') - is_packaged_as_dir = stat.S_ISDIR(mode) + is_packaged_as_dir = stat.S_ISDIR(rpm_mode) if is_packaged_as_dir and not is_listed_as_dir: self.output.add_info('W', pkg, 'permissions-dir-without-slash', path) elif is_listed_as_dir and not is_packaged_as_dir: self.output.add_info('W', pkg, 'permissions-file-as-dir', f'{path} is a file but listed as directory') - m = entry.mode - o = ':'.join((entry.owner, entry.group)) + entry_owner = ':'.join((entry.owner, entry.group)) - if stat.S_IMODE(mode) != m: - self.output.add_info('E', pkg, 'permissions-incorrect', f'{path} has mode 0{stat.S_IMODE(mode):o} but should be 0{m:o}') + if stat.S_IMODE(rpm_mode) != entry.mode: + self.output.add_info('E', pkg, 'permissions-incorrect', f'{path} has mode 0{stat.S_IMODE(rpm_mode):o} but should be 0{entry.mode:o}') - if owner != o: - self.output.add_info('E', pkg, 'permissions-incorrect-owner', f'{path} belongs to {owner} but should be {o}') + if rpm_owner != entry_owner: + self.output.add_info('E', pkg, 'permissions-incorrect-owner', f'{path} belongs to {rpm_owner} but should be {entry_owner}') - def _check_post_scriptlets(self, pkg, path, need_verifyscript): - script = pkg[rpm.RPMTAG_POSTIN] or pkg.scriptprog(rpm.RPMTAG_POSTINPROG) - found = False - need_set_permissions = False - - if script: - for line in script.split('\n'): - escaped = re.escape(path) - if re.search(fr'(chkstat|permctl) -n .* {escaped}', line): - found = True - break + def _check_post_scriptlets(self, pkg, path): + """Checks whether a call to "permctl -n {path}" is found in %post and + %verifyscript scriptlets of the package and complains if this is not + the case.""" + found_postin = self._lookup_permctl_call(path, pkg[rpm.RPMTAG_POSTIN] or pkg.scriptprog(rpm.RPMTAG_POSTINPROG)) + found_verify = self._lookup_permctl_call(path, pkg[rpm.RPMTAG_VERIFYSCRIPT] or pkg[rpm.RPMTAG_VERIFYSCRIPTPROG]) - # don't care about "static" entries that only serve as a kind of - # whitelisting purpose or sanity check that should only be applied - # during `chkstat --system` - if path in self.perms and self._is_static_entry(self.perms[path]): - return False + if not found_postin: + self.output.add_info('E', pkg, 'permissions-missing-postin', f'missing %set_permissions {path} in %post') - if need_verifyscript: - if not script or not found: - self.output.add_info('E', pkg, 'permissions-missing-postin', f'missing %set_permissions {path} in %post') - - need_set_permissions = True - script = (pkg[rpm.RPMTAG_VERIFYSCRIPT] or pkg[rpm.RPMTAG_VERIFYSCRIPTPROG]) - - found = False - if script: - for line in script.split('\n'): - escaped = re.escape(path) - if re.search(fr'(chkstat|permctl) -n .* {escaped}', line): - found = True - break + if not found_verify: + self.output.add_info('W', pkg, 'permissions-missing-verifyscript', f'missing %verify_permissions -e {path}') - if not script or not found: - self.output.add_info('W', pkg, 'permissions-missing-verifyscript', f'missing %verify_permissions -e {path}') + def _lookup_permctl_call(self, path, script): + """Checks whether a call to "permctl -n {path}" is present in the + given `script`.""" + import re - return need_set_permissions + if not script: + return False + + escaped = re.escape(path) + + for line in script.splitlines(): + if re.search(fr'(chkstat|permctl) -n .* {escaped}', line): + return True - def _is_static_entry(self, entry): - # entries coming from the fixed permissions profile are considered - # static - return entry.profile.endswith('/permissions') + return False @staticmethod def _paths_to(*file_names): @@ -108,44 +101,44 @@ yield f'{SHARE_DIR}/{name}' yield f'/etc/{name}' + def _is_static_entry(self, pkg, path): + for entry in self.perms.get(path, []): + if entry.matches_pkg(pkg.name) and entry.is_static(): + return True + + return False + def check(self, pkg): if pkg.is_source: return - permfiles = set() - # first pass, find and parse per-package drop-in files + dropin_files = set() + # first pass: find and parse per-package drop-in files, these take + # priority over the central profiles parsed in the constructor. for f in pkg.files.keys(): for prefix in list(self._paths_to('permissions.d/')) + [f'{SHARE_DIR}/packages.d/']: - if f.startswith(prefix): - if f in pkg.ghost_files: - continue - - dropin_dir = prefix.rstrip('/').split('/')[-1] - - # Attention: We require the FileDigestLocation config to - # mark all packages.d paths as "blacklisted" paths. - # e.g. [FileDigestLocation.permissions] with Locations - # /etc/permissions.d/ and /usr/share/permissions/permissions.d/ - # This ensures that an file-unauthorized error is thrown when an - # entry is not whitelisted. - # - # To whitelist a drop-in file after a successful review, - # the path and its digest need to be added as FileDigestCheck config - # having respective FileDigestLocation type (e.g. - # "permissions"). - # - # Here we add *all* files a package has in a dropin.d directory to our - # valid permissions files *without* checking if they belong - # to a whitelist as we assume it will be checked by - # FileDigestCheck and FileDigestLocation. - bn = f'{dropin_dir}/' + f[len(prefix):].split('.')[0] - if bn not in permfiles: - permfiles.add(bn) + if not f.startswith(prefix): + continue + elif f in pkg.ghost_files: + continue + + dropin_dir = os.path.basename(prefix.rstrip('/')) - for f in permfiles: + # these drop-in configuration files are whitelisted separately + # via permissions-whitelist.toml and are thus considered + # trusted. + + # we only add the basename of the drop-in configuration file + # without suffix. Below we'll lookup the .secure variant + # first, if existing, otherwise the basename. + bn = f'{dropin_dir}/' + f[len(prefix):].split('.')[0] + dropin_files.add(bn) + + for f in dropin_files: # check for a .secure file first, falling back to the plain file for path in self._paths_to(f + '.secure', f): if path in pkg.files: + # this path points to the extracted package directory tree fullpath = pkg.dir_name() + path try: self._parse_profile(fullpath) @@ -153,40 +146,60 @@ self.output.add_info('E', pkg, 'permissions-parse-error', f'{fullpath} caused a parsing error: {str(e)}.') break - need_set_permissions = False + # whether a PreReq for the permissions package will be needed in this RPM + requires_permctl = False for f, pkgfile in pkg.files.items(): if pkgfile.is_ghost: - # if a file is ghost we want to skip the files here. It's not + # We want to skip ghost files here. The package is not # actually shipping the file and we allow e.g. tmpfilesd to - # create entries with special permissions. If we would warn for - # these entries rpm -v will show errors. - # The drawback is that that we don't see warnings for privileges - # added by other mechanisms that are described in these %ghost - # files + # create entries with special permissions. If we would warn + # about these entries, rpm -v would show errors. The drawback + # is that that we don't see warnings for privileges added by + # other mechanisms that are described in these %ghost files continue if pkgfile.filecaps: + # capabilities are only assigned via permctl, should not be + # packaged directly self.output.add_info('E', pkg, 'permissions-fscaps', f"{f} has fscaps '{pkgfile.filecaps}'") mode = pkgfile.mode owner = pkgfile.user + ':' + pkgfile.group + # whether we need to check for invocation of permctl in %post or + # %verifyscript for this path + check_scriptlets = False + + skip_file = False + for entry in self.perms.get(f, []): + if entry.matches_pkg(pkg.name): + if stat.S_ISLNK(mode): + self.output.add_info('W', pkg, 'permissions-symlink', f) + skip_file = True + break - need_verifyscript = False - if f in self.perms: - if stat.S_ISLNK(mode): - self.output.add_info('W', pkg, 'permissions-symlink', f) - continue - - need_verifyscript = True - self._verify_entry(pkg, f, mode, owner) - - elif not stat.S_ISLNK(mode): - if mode & (stat.S_ISUID | stat.S_ISGID): - need_verifyscript = True - self._check_restricted_mode(pkg, f, mode) + check_scriptlets = True + self._verify_entry(entry, pkg, f, mode, owner) + break + else: + # no matching entry found; this means there is no whitelisting for any privileged bits. + if not stat.S_ISLNK(mode) and (mode & (stat.S_ISUID | stat.S_ISGID)): + check_scriptlets = True + self._complain_restricted_mode(pkg, f, mode) - if self._check_post_scriptlets(pkg, f, need_verifyscript): - need_set_permissions = True + if skip_file: + # is a symlink we warned about + continue - if need_set_permissions and 'permissions' not in [x[0] for x in pkg.prereq]: - self.output.add_info('E', pkg, 'permissions-missing-requires', "missing 'permissions' in PreReq") + if check_scriptlets: + # don't care about "static" entries that only serve a whitelisting + # purpose (e.g. directory sticky bits) or as a sanity check (e.g. safe + # permissions for /etc/ssh*). + # These permissions should already be correct after RPM install, + # so a call to permctl during %post is not strictly necessary. + if not self._is_static_entry(pkg, f): + self._check_post_scriptlets(pkg, f) + requires_permctl = True + + if requires_permctl: + if 'permissions' not in [x[0] for x in pkg.prereq]: + self.output.add_info('E', pkg, 'permissions-missing-requires', "missing 'permissions' in PreReq") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rpmlint-2.9.0+git20260211.ecf25fcf/rpmlint/permissions.py new/rpmlint-2.9.0+git20260211.72e34881/rpmlint/permissions.py --- old/rpmlint-2.9.0+git20260211.ecf25fcf/rpmlint/permissions.py 2026-02-11 09:10:05.000000000 +0100 +++ new/rpmlint-2.9.0+git20260211.72e34881/rpmlint/permissions.py 2026-02-11 17:27:36.000000000 +0100 @@ -10,7 +10,7 @@ class PermissionsEntry: - def __init__(self, profile, line_nr, path, owner, group, mode): + def __init__(self, profile, line_nr, path, owner, group, mode, packages): # source profile path self.profile = profile # source profile line nr @@ -24,9 +24,23 @@ self.caps = [] # related paths from variable expansions self.related_paths = [] + self.packages = packages + + def matches_pkg(self, pkg): + if not self.packages: + return True + return pkg in self.packages + + def is_static(self): + # entries coming from the fixed permissions profile are considered static + return self.profile.endswith('/permissions') def __str__(self): - ret = f'{self.profile}:{self.linenr}: {self.path} {self.owner}:{self.group} {oct(self.mode)}' + if self.packages: + package = f" (:package: {','.join(self.packages)})" + else: + package = '' + ret = f'{self.profile}:{self.linenr}:{package} {self.path} {self.owner}:{self.group} {oct(self.mode)}' for cap in self.caps: ret += '\n+capability ' + cap @@ -105,6 +119,7 @@ def __init__(self, var_handler, profile_path): self.var_handler = var_handler self.entries = {} + self._active_packages = [] with open(profile_path) as fd: self._parse_file(profile_path, fd) @@ -128,7 +143,7 @@ # "user:group" owner, group = ownership.replace('.', ':').split(':') mode = int(mode, 8) - entry = PermissionsEntry(context.label, context.line_nr, path, owner, group, mode) + entry = PermissionsEntry(context.label, context.line_nr, path, owner, group, mode, self._active_packages) expanded = self.var_handler.expand_paths(path) for p in expanded: @@ -139,7 +154,8 @@ # this is the root node, keep the slash key = '/' entry_copy = copy.deepcopy(entry) - self.entries[key] = entry_copy + entry_list = self.entries.setdefault(key, []) + entry_list.append(entry_copy) context.active_entries.append(entry_copy) elif line.startswith('+'): # capability line @@ -154,5 +170,8 @@ for entry in context.active_entries: entry.caps = caps + elif line.startswith(':package:'): + parts = line.split() + self._active_packages = parts[1].split(',') else: raise Exception(f'Unexpected line encountered in {context.label}:{context.line_nr}')
