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}')

Reply via email to