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-07-02 20:06:37
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/rpmlint (Old)
 and      /work/SRC/openSUSE:Factory/.rpmlint.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "rpmlint"

Thu Jul  2 20:06:37 2026 rev:542 rq:1362481 version:2.9.0+git20260629.bd947d09

Changes:
--------
--- /work/SRC/openSUSE:Factory/rpmlint/rpmlint.changes  2026-06-17 
16:18:00.026262920 +0200
+++ /work/SRC/openSUSE:Factory/.rpmlint.new.1982/rpmlint.changes        
2026-07-02 20:07:39.209638146 +0200
@@ -1,0 +2,20 @@
+Mon Jun 29 14:54:17 UTC 2026 - Wolfgang Frisch <[email protected]>
+
+- Update to version 2.9.0+git20260629.bd947d09:
+  * sysctl-whitelist: adjusted 50-coredump.conf digest for systemd - temporary 
revert (bsc#1267504)
+  * dbus-services: add txnupd-maintenance-tools service (bsc#1268577)
+  * test: add test coverage for new varlink related whitelisting features
+  * configs/openSUSE: add Varlink whitelisting restriction
+  * FileDigestCheck: add SocketUnitDigester
+  * FileDigestCheck: move digester types into utility module
+  * FileDigestCheck: support 'ContentCheck' configuration setting
+  * dbus-services: added kdeplasma6-addons kameleon.qmk whitelisting 
(bsc#1267818)
+
+-------------------------------------------------------------------
+Mon Jun 22 08:17:33 UTC 2026 - Wolfgang Frisch <[email protected]>
+
+- Update to version 2.9.0+git20260622.4dc32f49:
+  * sysctl-whitelist: adjusted 50-coredump.conf digest for systemd 
(bsc#1267504)
+  * configs/openSUSE/users-groups.toml: Replace group 'singularity' by 
'apptainer'
+
+-------------------------------------------------------------------

Old:
----
  rpmlint-2.9.0+git20260528.2490edb3.tar.xz

New:
----
  rpmlint-2.9.0+git20260629.bd947d09.tar.xz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ rpmlint.spec ++++++
--- /var/tmp/diff_new_pack.4izltp/_old  2026-07-02 20:07:40.729690684 +0200
+++ /var/tmp/diff_new_pack.4izltp/_new  2026-07-02 20:07:40.733690822 +0200
@@ -23,7 +23,7 @@
 %define name_suffix -%{flavor}
 %endif
 Name:           rpmlint%{name_suffix}
-Version:        2.9.0+git20260528.2490edb3
+Version:        2.9.0+git20260629.bd947d09
 Release:        0
 Summary:        RPM file correctness checker
 License:        GPL-2.0-or-later

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.4izltp/_old  2026-07-02 20:07:40.905696767 +0200
+++ /var/tmp/diff_new_pack.4izltp/_new  2026-07-02 20:07:40.917697182 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/rpm-software-management/rpmlint.git</param>
-              <param 
name="changesrevision">2490edb3f69bfb2c1df3829fe5f091651fde1f6f</param></service></servicedata>
+              <param 
name="changesrevision">bd947d09488873488dc1bd4174f143077694bf2d</param></service></servicedata>
 (No newline at EOF)
 

++++++ rpmlint-2.9.0+git20260528.2490edb3.tar.xz -> 
rpmlint-2.9.0+git20260629.bd947d09.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/dbus-services.toml 
new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/dbus-services.toml
--- old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/dbus-services.toml  
2026-05-28 14:31:50.000000000 +0200
+++ new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/dbus-services.toml  
2026-06-29 16:53:33.000000000 +0200
@@ -1616,8 +1616,8 @@
 
 [[FileDigestGroup]]
 package  = "kdeplasma6-addons"
-note     = "A small helper that allows to change LED colors via /sys/class/led"
-bug      = "bsc#1226306"
+note     = "A small helper that allows to change LED colors"
+bugs     = [ "bsc#1226306", "bsc#1267818" ]
 type     = "dbus"
 [[FileDigestGroup.digests]]
 path     = "/usr/share/dbus-1/system.d/org.kde.kameleonhelper.conf"
@@ -1627,6 +1627,14 @@
 path     = "/usr/share/dbus-1/system-services/org.kde.kameleonhelper.service"
 digester = "shell"
 hash     = "85e17072d140148cf4cb4186389f5dd6a24c56fbae76c5392504b9062560e0f6"
+[[FileDigestGroup.digests]]
+path     = 
"/usr/share/dbus-1/system-services/org.kde.kameleon.qmk.helper.service"
+digester = "shell"
+hash     = "7d11312252c8435071f453ee84a479366869bfeba72adb712f1bade9107e60ac"
+[[FileDigestGroup.digests]]
+path     = "/usr/share/dbus-1/system.d/org.kde.kameleon.qmk.helper.conf"
+digester = "xml"
+hash     = "fac2d51f124edeb87fb097aaaf53618744810d5ddcfc0fbab0232d2c55b81b30"
 
 [[FileDigestGroup]]
 package  = "supergfxctl"
@@ -1813,3 +1821,13 @@
 path     = "/usr/share/dbus-1/system.d/com.presire.qsnapper.Operations.conf"
 digester = "xml"
 hash     = "4ebd3fcf900e113e6d7a9a1e95ff7b32c4ab177723b8660030917313113a58ea"
+
+[[FileDigestGroup]]
+package  = "txnupd-maintenance-tools"
+note     = "forwards signals from transactional-update to a session D-Bus 
daemon"
+bug      = "bsc#1268577"
+type     = "dbus"
+[[FileDigestGroup.digests]]
+path     = "/usr/share/dbus-1/system.d/org.opensuse.tukit.Updated.conf"
+digester = "xml"
+hash     = "abf27e413feacafdb95e5b48ae2d75f501d506feb6a0a46a96fab338bde1fd13"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/opensuse.toml 
new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/opensuse.toml
--- old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/opensuse.toml       
2026-05-28 14:31:50.000000000 +0200
+++ new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/opensuse.toml       
2026-06-29 16:53:33.000000000 +0200
@@ -310,6 +310,10 @@
     "systemd-tmpfile-entry-unauthorized",
     "world-writable-mismatched-attrs",
     "world-writable-unauthorized-file",
+    "varlink-file-digest-mismatch",
+    "varlink-file-ghost",
+    "varlink-file-unauthorized",
+    "varlink-file-symlink",
     "zypperplugin-file-digest-mismatch",
     "zypperplugin-file-ghost",
     "zypperplugin-file-unauthorized"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/scoring.toml 
new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/scoring.toml
--- old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/scoring.toml        
2026-05-28 14:31:50.000000000 +0200
+++ new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/scoring.toml        
2026-06-29 16:53:33.000000000 +0200
@@ -98,6 +98,10 @@
 systemd-tmpfile-parse-error = 10
 systemd-tmpfile-symlink = 10
 missing-hash-section = 10000
+varlink-file-digest-mismatch = 10
+varlink-file-ghost = 10
+varlink-file-unauthorized = 10
+varlink-file-symlink = 10
 zypperplugin-file-digest-mismatch = 10
 zypperplugin-file-ghost = 10
 zypperplugin-file-unauthorized = 10
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/security.toml 
new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/security.toml
--- old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/security.toml       
2026-05-28 14:31:50.000000000 +0200
+++ new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/security.toml       
2026-06-29 16:53:33.000000000 +0200
@@ -18,6 +18,24 @@
     "/etc/dbus-1/system.d/"
 ]
 
+[FileDigestLocation.varlink]
+# At the moment there don't exist any symlinked socket units in Factory so
+# let's be restrictive here.
+FollowSymlinks = false
+# We are only looking for *.socket files in system service locations that
+# contain a "FileDescriptorName=varlink" directive. The ContentCheck will take
+# care of the latter limitation.
+Locations = [
+    "/etc/systemd/system/",
+    "/run/systemd/system/",
+    "/usr/lib/systemd/system/",
+    "/usr/local/lib/systemd/system/",
+]
+NamePatterns = [
+    "*.socket",
+]
+ContentCheck = "VarlinkServiceCheck"
+
 [FileDigestLocation.polkit]
 FollowSymlinks = false
 Locations = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/sysctl-whitelist.toml 
new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/sysctl-whitelist.toml
--- 
old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/sysctl-whitelist.toml   
    2026-05-28 14:31:50.000000000 +0200
+++ 
new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/sysctl-whitelist.toml   
    2026-06-29 16:53:33.000000000 +0200
@@ -52,7 +52,7 @@
 package = "systemd"
 type = "sysctl"
 note = "sets core pattern, core pipe limit and suid_dumpable"
-bugs = ["bsc#1174722", "bsc#1226865", "bsc#1243959"]
+bugs = ["bsc#1174722", "bsc#1226865", "bsc#1243959", "bsc#1267504"]
 [[FileDigestGroup.digests]]
 path = "/usr/lib/sysctl.d/50-coredump.conf"
 digester = "shell"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/users-groups.toml 
new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/users-groups.toml
--- old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/users-groups.toml   
2026-05-28 14:31:50.000000000 +0200
+++ new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/users-groups.toml   
2026-06-29 16:53:33.000000000 +0200
@@ -2,6 +2,7 @@
     'aegis',
     'alloy',
     'antivir',
+    'apptainer',
     'arangodb',
     'at',
     'audio',
@@ -180,7 +181,6 @@
     'shadow',
     'shibd',
     'signaling',
-    'singularity',
     'siproxd',
     '_sks',
     'slurm',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/varlink-whitelist.toml 
new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/varlink-whitelist.toml
--- 
old/rpmlint-2.9.0+git20260528.2490edb3/configs/openSUSE/varlink-whitelist.toml  
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/rpmlint-2.9.0+git20260629.bd947d09/configs/openSUSE/varlink-whitelist.toml  
    2026-06-29 16:53:33.000000000 +0200
@@ -0,0 +1,9 @@
+[[FileDigestGroup]]
+package  = "rpmlint-integration-test"
+type     = "varlink"
+note     = "valid test whitelisting entry for the OBS integration test package"
+bug      = "bsc#1188704"
+[[FileDigestGroup.digests]]
+path     = "/usr/lib/systemd/system/test.socket"
+digester = "systemd-socket"
+hash     = "ff7a1f7581f81790cc063071026c4deaf4eafa6b0658217250db8e420c9064bc"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/rpmlint/checks/FileDigestCheck.py 
new/rpmlint-2.9.0+git20260629.bd947d09/rpmlint/checks/FileDigestCheck.py
--- old/rpmlint-2.9.0+git20260528.2490edb3/rpmlint/checks/FileDigestCheck.py    
2026-05-28 14:31:50.000000000 +0200
+++ new/rpmlint-2.9.0+git20260629.bd947d09/rpmlint/checks/FileDigestCheck.py    
2026-06-29 16:53:33.000000000 +0200
@@ -2,124 +2,22 @@
 from fnmatch import fnmatch
 import hashlib
 from pathlib import Path
-import re
 import stat
-import xml.etree.ElementTree as ET
 
 from rpmlint.checks.AbstractCheck import AbstractCheck
+import rpmlint.filedigestcheck as check_utils
 
 
 DEFAULT_DIGEST_ALG = 'sha256'
 
-
-# We support different "filters" for calculating file digests for whitelisting
-# purposes. The reasoning behind this is that we want to detect *functional*
-# changes to certain files but we don't want to be bothered by any other
-# changes like changes in comments, copyright headers, whitespace and so on.
-#
-# To avoid such bogus changes showing up in the checker we can filter the
-# target files depending on their file type. Currently we have two common
-# special cases:
-#
-# - shell-like files (configuration files, shell scripts, Python
-# scripts, Perl scripts etc) that can contain empty lines or comments
-# introduced by '#'.
-# - XML files for things like D-Bus configuration files or polkit policies.
-# Here also XML style comments and whitespace can occur.
-#
-# The filter classes help in calculating a file digest for a filtered version
-# of the target file that is normalized and therefore stable against the
-# outlined kinds of changes. Since this makes getting the right whitelisting
-# digests hard, a small companion tool in tools/get_whitelisting_digest.py
-# exists.
-
-
-class DefaultDigester:
-    """This class performs the default digest calculation of arbitrary file
-    contents as is."""
-
-    def __init__(self, path, halg):
-        self.path = path
-        self.halg = halg
-
-    def parse_content(self):
-        # simply generate chunks on binary level
-        with open(self.path, 'rb') as fd:
-            while True:
-                chunk = fd.read(4096)
-                if not chunk:
-                    break
-                yield chunk
-
-    def get_digest(self):
-        """This returns the hash digest of the *un*filtered input file."""
-        hasher = hashlib.new(self.halg)
-
-        for chunk in self.parse_content():
-            hasher.update(chunk)
-
-        return hasher.hexdigest()
-
-
-class ShellDigester(DefaultDigester):
-    """This class performs digest calculation of shell style configuration
-    files or scripts. Certain aspects like comments and whitespace will be
-    filtered out of the digest calculation."""
-
-    def parse_content(self):
-        # generate filtered lines
-        with open(self.path) as fd:
-            for line_nr, line in enumerate(fd):
-                stripped = line.strip()
-                if not stripped:
-                    # skip empty lines
-                    continue
-                elif line_nr == 0 and stripped.startswith('#!'):
-                    # keep shebang lines mostly intact,
-                    # but ignore minor interpreter versions
-                    line = re.sub(r'\bpython3\.\d+\b', 'python3', line)
-                elif stripped.startswith('#'):
-                    # skip comments
-                    # NOTE: we don't strip trailing comments like in
-                    # 'if [ 5 -eq $NUM ]; then # compare to 5'
-                    # because the danger would be too high to remove actual
-                    # content instead of just comments
-                    continue
-
-                # don't use the completely stripped version, in Python for
-                # example the indentation is part of the syntax and changes
-                # here could change the meaning
-                yield (line.rstrip() + '\n').encode()
-
-
-class XmlDigester(DefaultDigester):
-    """This class performs digest calculation of XML configuration files.
-    Certain aspects like comments and whitespace will be filtered out of the
-    digest calculation."""
-
-    def parse_content(self):
-        # NOTE: the ElementTree is not robust against malicious input. Rpmlint
-        # processes a lot of untrusted input. In the OBS context this only
-        # happens within the virtual machines where packagers can manipulate a
-        # lot of things anyway so it should be okay.
-
-        # this returns a canonicalized form of the XML without comments or
-        # anything. It is missing the XML preamble and DOCTYPE declaration,
-        # though. It's as good as I could get it to be without parsing XML on
-        # foot or relying on third party XML modules.
-
-        # chunked / line wise processing is likely impossible for XML so
-        # return the whole bunch
-        yield ET.canonicalize(from_file=self.path, strip_text=True).encode()
-
-
 # These values can be used in the individual "digests" entries of a
 # whitelisting entry for a given digest group. For example:
 # { path = "/some/path.xml", algorithm = "sha256", digester = "xml", hash = 
"..." }
 DIGESTERS = {
-    'default': DefaultDigester,
-    'shell': ShellDigester,
-    'xml': XmlDigester,
+    'default': check_utils.DefaultDigester,
+    'shell': check_utils.ShellDigester,
+    'xml': check_utils.XmlDigester,
+    'systemd-socket': check_utils.SocketUnitDigester
 }
 
 
@@ -147,6 +45,7 @@
             # make sure these keys always exists
             config.setdefault('NamePatterns', [])
             config.setdefault('FollowSymlinks', False)
+            config.setdefault('ContentCheck', None)
             config['type'] = check_type
             self.checks.append(config)
             self.known_check_types[check_type] = config
@@ -406,6 +305,12 @@
             digest_hint += f' of resolved path {pkgfile.name}'
 
         for dtype, digester in DIGESTERS.items():
+            # lex systemd-socket: avoid spamming the output (and breaking
+            # expected test case output) with systemd-socket digests we don't
+            # need.
+            if dtype == 'systemd-socket' and not path.endswith('.socket'):
+                continue
+
             try:
                 digest = self._calc_digest(digester, pkgfile, 
DEFAULT_DIGEST_ALG)
             except Exception:
@@ -657,6 +562,30 @@
                 return True
         return False
 
+    def has_restricted_content(self, check, pkg, path):
+        """Check whether the given path, which is located in a restricted
+        location, contains data which makes it subject to our whitelisting
+        restriction."""
+        content_check = check['ContentCheck']
+        if not content_check:
+            # there is no content check type declared in the configuration,
+            # this means all files present in the restricted location are
+            # covered by the check.
+            return True
+
+        pkgfile = self._resolve_links(pkg, path)
+        if pkgfile is None:
+            # some error in link resolution, later checks will complain more
+            # explicitly about this.
+            return True
+
+        try:
+            ContentChecker = getattr(check_utils, content_check)
+        except AttributeError:
+            raise Exception(f'No matching type for 
ContentCheck={content_check} found in rpmlint.filedigestcheck')
+        checker = ContentChecker()
+        return checker.is_restricted(pkgfile.path)
+
     def check_binary(self, pkg):
         """Entry point for digest checks. Check that all files in restricted
         locations are covered by a file digest group in which all files have
@@ -681,6 +610,10 @@
             elif stat.S_ISLNK(pkgfile.mode) and not check['FollowSymlinks']:
                 if not self._is_symlink_allowed(pkg, path):
                     self.output.add_info('E', pkg, 
f'{check_type}-file-symlink', path)
+            elif not self.has_restricted_content(check, pkg, path):
+                # the path is in a restricted location but the content is not
+                # relevant to us.
+                continue
             else:
                 file_list = restricted_paths.setdefault(check_type, [])
                 file_list.append(path)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/rpmlint/descriptions/FileDigestCheck.toml
 
new/rpmlint-2.9.0+git20260629.bd947d09/rpmlint/descriptions/FileDigestCheck.toml
--- 
old/rpmlint-2.9.0+git20260528.2490edb3/rpmlint/descriptions/FileDigestCheck.toml
    2026-05-28 14:31:50.000000000 +0200
+++ 
new/rpmlint-2.9.0+git20260629.bd947d09/rpmlint/descriptions/FileDigestCheck.toml
    2026-06-29 16:53:33.000000000 +0200
@@ -38,6 +38,11 @@
 sysctl-file-ghost="This package installs a sysctl.d drop-in configuration file 
as a %ghost file. #GHOST_ENCOUNTERED_TEXT#"
 sysctl-file-symlink="Symlinks are not allowed for sysctl.d drop-in 
configuration files"
 
+varlink-file-digest-mismatch="A systemd socket unit declaring a Varlink 
service changed in content. Packaging varlink services #SUFFIX#. 
#REVIEW_NEEDED_TEXT#"
+varlink-file-ghost="This package installs a systemd socket unit declaring a 
Varlink service as a %ghost file. #GHOST_ENCOUNTERED_TEXT#"
+varlink-file-symlink="Symlinks are not allowed for systemd socket units 
declaring Varlink services"
+varlink-file-unauthorized="Packaging new Varlink services #SUFFIX#. 
#REVIEW_NEEDED_TEXT#"
+
 zypperplugin-file-digest-mismatch="A whitelisting related zypper plugin file 
changed in content. Packaging zypper plugins #SUFFIX#. #REVIEW_NEEDED_TEXT#"
 zypperplugin-file-unauthorized="Packaging new zypper plugins #SUFFIX#. 
#REVIEW_NEEDED_TEXT#"
 zypperplugin-file-ghost="This package installs a zypper plugin as a %ghost 
file. #GHOST_ENCOUNTERED_TEXT#"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/rpmlint/filedigestcheck.py 
new/rpmlint-2.9.0+git20260629.bd947d09/rpmlint/filedigestcheck.py
--- old/rpmlint-2.9.0+git20260528.2490edb3/rpmlint/filedigestcheck.py   
1970-01-01 01:00:00.000000000 +0100
+++ new/rpmlint-2.9.0+git20260629.bd947d09/rpmlint/filedigestcheck.py   
2026-06-29 16:53:33.000000000 +0200
@@ -0,0 +1,223 @@
+import configparser
+
+# This module contains helper types used by the FileDigestCheck to implement
+# configuration-specific logic.
+
+# ContentCheck types
+#
+# The following types relate to the "ContentCheck" configuration setting,
+# which allows to specify a class name in this module which will be invoked to
+# determine whether the whitelisting restriction should be applied to a
+# specific file. A type needs to provide a is_restricted(self, path: str)
+# member function to be usable as "ContentCheck".
+
+
+def _open_socket_config(path):
+    """Opens a systemd .socket unit in `path` as an INI configuration file and
+    returns the ConfigParser instance resulting from it. On error None is
+    returned."""
+    # systemd services files are not 100 % compatible with regular INI
+    # files, thus we make python's configparser a bit more relaxed.
+    config = configparser.ConfigParser(
+        interpolation=None,
+        strict=False
+    )
+
+    # by default option keys are converted to lower-case, we don't want that
+    config.optionxform = lambda opt: opt
+
+    read = config.read(path)
+    if len(read) == 1:
+        return config
+
+    return None
+
+
+class VarlinkServiceCheck:
+    """Checks a systemd .socket unit file whether it refers to a Varlink
+    service."""
+
+    def is_restricted(self, path: str):
+
+        config = _open_socket_config(path)
+
+        if not config:
+            # failed to parse the file, assume it is restricted
+            return True
+
+        try:
+            socket_section = config['Socket']
+        except KeyError:
+            # no socket section in a socket unit? not Varlink anyway.
+            return False
+
+        try:
+            file_descriptor_name = socket_section['FileDescriptorName']
+        except KeyError:
+            # no varlink service
+            return False
+
+        # this is more of a convention, not a hard requirement, but so far all
+        # Varlink services we have use this scheme.
+        return file_descriptor_name == 'varlink'
+
+# Digester types
+
+# We support different "filters" for calculating file digests for whitelisting
+# purposes. The reasoning behind this is that we want to detect *functional*
+# changes to certain files but we don't want to be bothered by any other
+# changes like changes in comments, copyright headers, whitespace and so on.
+#
+# To avoid such bogus changes showing up in the checker we can filter the
+# target files depending on their file type. Currently we have two common
+# special cases:
+#
+# - shell-like files (configuration files, shell scripts, Python
+# scripts, Perl scripts etc) that can contain empty lines or comments
+# introduced by '#'.
+# - XML files for things like D-Bus configuration files or polkit policies.
+# Here also XML style comments and whitespace can occur.
+#
+# The filter classes help in calculating a file digest for a filtered version
+# of the target file that is normalized and therefore stable against the
+# outlined kinds of changes. Since this makes getting the right whitelisting
+# digests hard, a small companion tool in tools/get_whitelisting_digest.py
+# exists.
+
+
+class DefaultDigester:
+    """This class performs the default digest calculation of arbitrary file
+    contents as is."""
+
+    def __init__(self, path, halg):
+        self.path = path
+        self.halg = halg
+
+    def parse_content(self):
+        # simply generate chunks on binary level
+        with open(self.path, 'rb') as fd:
+            while True:
+                chunk = fd.read(4096)
+                if not chunk:
+                    break
+                yield chunk
+
+    def get_digest(self):
+        """This returns the hash digest of the *un*filtered input file."""
+        import hashlib
+        hasher = hashlib.new(self.halg)
+
+        for chunk in self.parse_content():
+            hasher.update(chunk)
+
+        return hasher.hexdigest()
+
+
+class ShellDigester(DefaultDigester):
+    """This class performs digest calculation of shell style configuration
+    files or scripts. Certain aspects like comments and whitespace will be
+    filtered out of the digest calculation."""
+
+    def parse_content(self):
+        import re
+
+        # generate filtered lines
+        with open(self.path) as fd:
+            for line_nr, line in enumerate(fd):
+                stripped = line.strip()
+                if not stripped:
+                    # skip empty lines
+                    continue
+                elif line_nr == 0 and stripped.startswith('#!'):
+                    # keep shebang lines mostly intact,
+                    # but ignore minor interpreter versions
+                    line = re.sub(r'\bpython3\.\d+\b', 'python3', line)
+                elif stripped.startswith('#'):
+                    # skip comments
+                    # NOTE: we don't strip trailing comments like in
+                    # 'if [ 5 -eq $NUM ]; then # compare to 5'
+                    # because the danger would be too high to remove actual
+                    # content instead of just comments
+                    continue
+
+                # don't use the completely stripped version, in Python for
+                # example the indentation is part of the syntax and changes
+                # here could change the meaning
+                yield (line.rstrip() + '\n').encode()
+
+
+class XmlDigester(DefaultDigester):
+    """This class performs digest calculation of XML configuration files.
+    Certain aspects like comments and whitespace will be filtered out of the
+    digest calculation."""
+
+    def parse_content(self):
+        import xml.etree.ElementTree as ET
+        # NOTE: the ElementTree is not robust against malicious input. Rpmlint
+        # processes a lot of untrusted input. In the OBS context this only
+        # happens within the virtual machines where packagers can manipulate a
+        # lot of things anyway so it should be okay.
+
+        # this returns a canonicalized form of the XML without comments or
+        # anything. It is missing the XML preamble and DOCTYPE declaration,
+        # though. It's as good as I could get it to be without parsing XML on
+        # foot or relying on third party XML modules.
+
+        # chunked / line wise processing is likely impossible for XML so
+        # return the whole bunch
+        yield ET.canonicalize(from_file=self.path, strip_text=True).encode()
+
+
+class SocketUnitDigester(DefaultDigester):
+
+    # [Socket] section configuration keys to include in the digest
+    # calculation.
+    #
+    # see systemd.socket(5) man page for reference.
+    #
+    # we're only interested in settings related to API / permissions / network
+    # entry points.
+    KEYS_TO_HASH = {
+        'ListenStream',
+        'ListenDatagram',
+        'ListenSequentialPacket',
+        'ListenFIFO',
+        'ListenSpecial',
+        'ListenNetlink',
+        'ListenMessageQueue',
+        'SocketProtocol',
+        'BindToDevice',
+        'SocketUser',
+        'SocketGroup',
+        'SocketMode',
+        'DirectoryMode',
+        'PassSecurity',
+        'AcceptFileDescriptors',
+        'ExecStartPre',
+        'ExecStartPost',
+        'ExecStopPre',
+        'ExecStopPost',
+        'FileDescriptorName',
+        'PassFileDescriptorsToExec'
+    }
+
+    def parse_content(self):
+        config = _open_socket_config(self.path)
+
+        if not config:
+            # shouldn't happen since VarlinkServiceCheck above should have
+            # already read the file successfully before.
+            raise Exception(f'failed to parse {self.path}')
+
+        try:
+            socket_section = config['Socket']
+        except KeyError:
+            # VarlinkServiceCheck must have seen a [Socket] section so this
+            # shouldn't happen.
+            raise Exception(f'[Socket] section in {self.path} disappeared?')
+
+        # yield all key/value pairs we're interested in for hashing
+        for key in socket_section:
+            if key in self.KEYS_TO_HASH:
+                value = socket_section[key]
+                yield f'{key}={value}\n'.encode()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/test/configs/digests_varlink.config 
new/rpmlint-2.9.0+git20260629.bd947d09/test/configs/digests_varlink.config
--- old/rpmlint-2.9.0+git20260528.2490edb3/test/configs/digests_varlink.config  
1970-01-01 01:00:00.000000000 +0100
+++ new/rpmlint-2.9.0+git20260629.bd947d09/test/configs/digests_varlink.config  
2026-06-29 16:53:33.000000000 +0200
@@ -0,0 +1,20 @@
+[FileDigestLocation.varlinktest]
+FollowSymlinks = false
+Locations = [
+    "/sockets/",
+]
+NamePatterns = [
+    "*.socket",
+]
+ContentCheck = "VarlinkServiceCheck"
+
+[[FileDigestGroup]]
+package = "socketpkg"
+type = "varlinktest"
+note = "tests a package which as a systemd.socket unit"
+bug = "bsc#4321"
+[[FileDigestGroup.digests]]
+path = "/sockets/the.socket"
+algorithm = "sha256"
+digester = "systemd-socket"
+hash = "7978041027394b996610b0de563a83fea8ad425c1f6837e57c1b65863dac9933"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/test/test_file_digest.py 
new/rpmlint-2.9.0+git20260629.bd947d09/test/test_file_digest.py
--- old/rpmlint-2.9.0+git20260528.2490edb3/test/test_file_digest.py     
2026-05-28 14:31:50.000000000 +0200
+++ new/rpmlint-2.9.0+git20260629.bd947d09/test/test_file_digest.py     
2026-06-29 16:53:33.000000000 +0200
@@ -406,6 +406,60 @@
         assert len(output.results) == 1
 
 
+def test_varlink_content_check():
+    # here we want to test whether the VarlinkServiceCheck ContentCheck
+    # correctly identifies socket units declaring Varlink sockets.
+    UNAFFECTED_SOCKET_DATA = '[Socket]\nThis=stuff\n'
+
+    output, test = get_digestcheck('digests_varlink.config')
+    with FakePkg('defaultpkg') as pkg:
+        pkg.add_file_with_content('/sockets/harmless.socket', 
UNAFFECTED_SOCKET_DATA)
+        test.check(pkg)
+        assert len(output.results) == 0
+
+    # only this config should be complained about since if contains a varlink
+    # FileDescriptorName
+    RESTRICTED_SOCKET_DATA = '[Socket]\nFileDescriptorName=varlink\n'
+
+    output, test = get_digestcheck('digests_varlink.config')
+    with FakePkg('defaultpkg') as pkg:
+        pkg.add_file_with_content('/sockets/evil.socket', 
RESTRICTED_SOCKET_DATA)
+        test.check(pkg)
+        assert len(output.results) == 1
+
+
+def test_varlink_digester():
+    # here we want to check the SocketUnitDigester:
+    # - reject a mismatching digest
+    # - accept a matching digest
+    # - accept a file which has only changes in uninteresting fields
+
+    MISMATCHING_SOCKET_DATA = 
'[Socket]\nFileDescriptorName=varlink\nSocketMode=0606\n'
+
+    output, test = get_digestcheck('digests_varlink.config')
+    with FakePkg('socketpkg') as pkg:
+        pkg.add_file_with_content('/sockets/the.socket', 
MISMATCHING_SOCKET_DATA)
+        test.check(pkg)
+        assert len(output.results) == 1
+
+    MATCHING_SOCKET_DATA = 
'[Socket]\nFileDescriptorName=varlink\nSocketMode=0777\n'
+
+    output, test = get_digestcheck('digests_varlink.config')
+    with FakePkg('socketpkg') as pkg:
+        pkg.add_file_with_content('/sockets/the.socket', MATCHING_SOCKET_DATA)
+        test.check(pkg)
+        assert len(output.results) == 0
+
+    # this adds Backlog=100, an uninteresting key, should yield the same digest
+    SIMILAR_SOCKET_DATA = 
'[Socket]\nFileDescriptorName=varlink\nSocketMode=0777\nBacklog=100\n'
+
+    output, test = get_digestcheck('digests_varlink.config')
+    with FakePkg('socketpkg') as pkg:
+        pkg.add_file_with_content('/sockets/the.socket', SIMILAR_SOCKET_DATA)
+        test.check(pkg)
+        assert len(output.results) == 0
+
+
 @pytest.mark.parametrize('package', ['binary/pam-module'])
 def test_pam_modules(tmp_path, package, digestcheck):
     output, test = get_digestcheck('digests_pam.config')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rpmlint-2.9.0+git20260528.2490edb3/tools/get_whitelisting_digest.py 
new/rpmlint-2.9.0+git20260629.bd947d09/tools/get_whitelisting_digest.py
--- old/rpmlint-2.9.0+git20260528.2490edb3/tools/get_whitelisting_digest.py     
2026-05-28 14:31:50.000000000 +0200
+++ new/rpmlint-2.9.0+git20260629.bd947d09/tools/get_whitelisting_digest.py     
2026-06-29 16:53:33.000000000 +0200
@@ -7,6 +7,7 @@
 sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
 
 import rpmlint.checks.FileDigestCheck as fdc  # noqa: E402
+import rpmlint.filedigestcheck as fdc_utils  # noqa: E402
 
 parser = argparse.ArgumentParser(
     description='This tool helps calculating whitelisting digests for the 
FileDigestCheck'
@@ -23,7 +24,7 @@
     print('Cannot output result data for default digester (which is binary)', 
file=sys.stderr)
     sys.exit(1)
 
-Digester = fdc.DIGESTERS.get(args.filter, fdc.DefaultDigester)
+Digester = fdc.DIGESTERS.get(args.filter, fdc_utils.DefaultDigester)
 
 digester = Digester(args.FILE, fdc.DEFAULT_DIGEST_ALG)
 

Reply via email to