Skip to content

Commit af3c4bc

Browse files
committed
Merge branch 'main' into opensuse
2 parents 21d9406 + fbb5765 commit af3c4bc

8 files changed

Lines changed: 177 additions & 14 deletions

File tree

configs/openSUSE/opensuse.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ UseVarLockSubsys = false
66
UseVersionInChangelog = false
77
BadnessThreshold = 999
88

9+
# Set to true to issue a warning for ghost entries outside snapshots
10+
# when checking for atomic update compatibility
11+
AtomicCheckGhosts = false
12+
913
# Enabled checks for the rpmlint to be run (besides the default set)
1014
Checks = [
1115
"BashismsCheck",
@@ -24,13 +28,33 @@ Checks = [
2428
"SystemdTmpfilesCheck",
2529
"SUIDPermissionsCheck",
2630
"WorldWritableCheck",
31+
"AtomicUpdateCheck",
2732
]
2833

2934
# List of directory prefixes that are not allowed in packages
3035
DisallowedDirs = [
3136
"/etc/NetworkManager/dispatcher.d",
3237
]
3338

39+
# Only these directories may be used by packages compatible with
40+
# atomic updates
41+
AtomicAllowedDirs = [
42+
"/etc/",
43+
"/usr/",
44+
"/bin/",
45+
"/lib/",
46+
"/lib64/",
47+
"/sbin/",
48+
"/boot/",
49+
]
50+
51+
# List of subdirectories which are disallowed for atomic updates
52+
# despite being within otherwise allowed directories
53+
AtomicDisallowedSubdirs = [
54+
"/usr/local/",
55+
"/boot/efi/",
56+
]
57+
3458
FilterErrorTitles = [
3559
'cross-directory-hard-link',
3660
]
@@ -83,6 +107,7 @@ Filters = [
83107
'^filesystem\..*: dir-or-file-in-tmp',
84108
'^filesystem\..*: dir-or-file-in-mnt',
85109
'^filesystem\..*: dir-or-file-in-home',
110+
'^filesystem\..*: dir-or-file-outside-snapshot',
86111
'^filesystem\..*: hidden-file-or-dir /root/.gnupg',
87112
'^filesystem\..*: hidden-file-or-dir /root/.gnupg',
88113
'^filesystem\..*: hidden-file-or-dir /etc/skel/.config',

configs/openSUSE/scoring.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,7 @@ zypperplugin-file-ghost = 10
100100
zypperplugin-file-unauthorized = 10
101101
patch-macro-old-format = 10000
102102
logrotate-user-writable-log-dir = 10000
103+
104+
# Set to 10000 once affected packages have been updated
105+
# for atomic update compatibility
106+
dir-or-file-outside-snapshot = 100
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from rpmlint.checks.AbstractCheck import AbstractCheck
2+
3+
4+
class AtomicUpdateCheck(AbstractCheck):
5+
6+
"""
7+
Requirements for atomic updates:
8+
* All files must be stored inside the snapshot, which is in our case /etc and /usr, not /var,
9+
/opt, /srv, /usr/local or anything else.
10+
* (Re)starting daemons is not possible.
11+
* Modifying files outside of /usr and /etc is not possible.
12+
* Modifications outside the snapshot have to be done via systemd-tmpfiles and systemd services.
13+
This check currently only implements checking for files at illegal paths.
14+
"""
15+
16+
def __init__(self, config, output):
17+
super().__init__(config, output)
18+
self.check_ghosts = self.config.configuration['AtomicCheckGhosts']
19+
self.allowed_dirs = self.config.configuration['AtomicAllowedDirs']
20+
self.disallowed_subdirs = self.config.configuration['AtomicDisallowedSubdirs']
21+
22+
def check(self, pkg):
23+
if pkg.is_source:
24+
return
25+
26+
# Check for files stored outside the snapshot
27+
self._check_paths(pkg, self.check_ghosts)
28+
29+
def _check_paths(self, pkg, check_ghosts=False):
30+
for file in pkg.files.keys():
31+
if file in pkg.ghost_files:
32+
continue # Ghosts are only handled if explicitly desired
33+
if not (self._check_single_path(file)):
34+
self.output.add_info('E', pkg, 'dir-or-file-outside-snapshot', file)
35+
if check_ghosts:
36+
for ghost in pkg.ghost_files:
37+
if not (self._check_single_path(ghost)):
38+
self.output.add_info('W', pkg, 'ghost-outside-snapshot', ghost)
39+
40+
def _check_single_path(self, file):
41+
return (
42+
file.startswith(tuple(self.allowed_dirs)) and
43+
not file.startswith(tuple(self.disallowed_subdirs))
44+
)

rpmlint/cli.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def process_lint_args(argv):
7575
parser.add_argument('-V', '--version', action='version', version=__version__, help='show package version and exit')
7676
parser.add_argument('-c', '--config', type=_validate_conf_location, help='load up additional configuration data from specified path (file or directory with *.toml files)')
7777
parser.add_argument('-e', '--explain', nargs='+', default='', help='provide detailed explanation for one specific message id')
78-
parser.add_argument('-r', '--rpmlintrc', '--file', type=_is_file_path, help='load up specified rpmlintrc file')
78+
parser.add_argument('-r', '--rpmlintrc', '--file', action='append', type=_is_file_path, help='load up specified rpmlintrc file (may be repeated)')
7979
parser.add_argument('-v', '--verbose', '--info', action='store_true', help='provide detailed explanations where available')
8080
parser.add_argument('-p', '--print-config', action='store_true', help='print the settings that are in effect when using the rpmlint')
8181
parser.add_argument('-i', '--installed', nargs='+', default='', help='installed packages to be validated by rpmlint')
@@ -97,16 +97,7 @@ def process_lint_args(argv):
9797

9898
options = parser.parse_args(args=argv)
9999

100-
# make sure rpmlintrc exists
101-
if options.rpmlintrc:
102-
if not options.rpmlintrc.exists():
103-
print_warning(f"User specified rpmlintrc '{options.rpmlintrc}' does not exist")
104-
sys.exit(2)
105-
# make it a list
106-
options.rpmlintrc = [options.rpmlintrc]
107-
else:
108-
options.rpmlintrc = []
109-
# validate all the rpmlfile options to be either file or folder
100+
# validate all the rpmfile options to be either file or folder
110101
f_path = set()
111102
invalid_path = False
112103
for item in options.rpmfile:

rpmlint/configdefaults.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ Filters = []
3636
BlockedFilters = []
3737
# Treshold where we should error out, by default single error is enough
3838
BadnessThreshold = -1
39+
# Set to true to issue a warning for ghost entries outside snapshots
40+
# when checking for atomic update compatibility
41+
AtomicCheckGhosts = false
3942
# When checking that various files that should be compressed are
4043
# indeed compressed, look for this filename extension
4144
CompressExtension = "bz2"
@@ -213,6 +216,26 @@ DisallowedDirs = [
213216
"/var/run",
214217
"/var/tmp",
215218
]
219+
220+
# Only these directories may be used by packages compatible with
221+
# atomic updates
222+
AtomicAllowedDirs = [
223+
"/etc/",
224+
"/usr/",
225+
"/bin/",
226+
"/lib/",
227+
"/lib64/",
228+
"/sbin/",
229+
"/boot/",
230+
]
231+
232+
# List of subdirectories which are disallowed for atomic updates
233+
# despite being within otherwise allowed directories
234+
AtomicDisallowedSubdirs = [
235+
"/usr/local/",
236+
"/boot/efi/",
237+
]
238+
216239
# Standard OS groups
217240
StandardGroups = [
218241
"root",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
dir-or-file-outside-snapshot="""
2+
The package contains files outside the snapshot, e.g. outside /etc and /usr
3+
or inside /usr/local.
4+
"""
5+
ghost-outside-snapshot="""
6+
The package contains ghosts outside the snapshot, e.g. outside /etc and /usr
7+
or inside /usr/local. This might become an issue upon removal of this
8+
package, but not during installation.
9+
"""

rpmlint/lint.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,9 @@ def _load_rpmlintrc(self):
214214
pkg = pkg.parent
215215
self.options['rpmlintrc'] += self._find_rpmlintrc_files(pkg)
216216

217-
if len(self.options['rpmlintrc']) > 1:
218-
# multiple rpmlintrcs are highly undesirable
219-
print_warning('There are multiple items to be loaded: {}.'.format(' '.join(map(str, self.options['rpmlintrc']))))
217+
if len(self.options['rpmlintrc']) > 1:
218+
# multiple rpmlintrcs are highly undesirable
219+
print_warning('There are multiple items to be loaded: {}.'.format(' '.join(map(str, self.options['rpmlintrc']))))
220220
for rcfile in self.options['rpmlintrc']:
221221
self.config.load_rpmlintrc(rcfile)
222222

test/test_atomic_update.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import pytest
2+
import rpm
3+
from rpmlint.checks.AtomicUpdateCheck import AtomicUpdateCheck
4+
from rpmlint.filter import Filter
5+
6+
from Testing import CONFIG, get_tested_mock_package
7+
8+
9+
@pytest.fixture(scope='function', autouse=True)
10+
def atomiccheck():
11+
CONFIG.info = True
12+
CONFIG.configuration['AtomicCheckGhosts'] = True
13+
output = Filter(CONFIG)
14+
test = AtomicUpdateCheck(CONFIG, output)
15+
yield output, test
16+
17+
18+
@pytest.fixture
19+
def output(atomiccheck):
20+
output, _test = atomiccheck
21+
yield output
22+
23+
24+
@pytest.fixture
25+
def test(atomiccheck):
26+
_output, test = atomiccheck
27+
yield test
28+
29+
30+
@pytest.mark.parametrize('package', [
31+
get_tested_mock_package(files=('/var/lib/pipewire',)),
32+
get_tested_mock_package(files=('/opt/bin/test',)),
33+
get_tested_mock_package(files=('/usr/local/bin/test',)),
34+
get_tested_mock_package(files=('/boot/efi/test',)),
35+
])
36+
def test_not_atomic(package, output, test):
37+
test.check(package)
38+
out = output.print_results(output.results)
39+
assert 'E: dir-or-file-outside-snapshot' in out
40+
41+
42+
@pytest.mark.parametrize('package', [
43+
get_tested_mock_package(files=('/etc/custom.config',)),
44+
get_tested_mock_package(files=('/usr/lib64/libc.so',)),
45+
get_tested_mock_package(files=('/usr/etc/nfs.conf',)),
46+
get_tested_mock_package(files=('/bin/test',)),
47+
get_tested_mock_package(files=('/sbin/test',)),
48+
get_tested_mock_package(files=('/lib/libc.so',)),
49+
get_tested_mock_package(files=('/lib64/libc.so',)),
50+
get_tested_mock_package(files=('/boot/grub2/grub.cfg',)),
51+
])
52+
def test_atomic(package, output, test):
53+
test.check(package)
54+
out = output.print_results(output.results)
55+
assert 'E: dir-or-file-outside-snapshot' not in out
56+
assert 'W: ghost-outside-snapshot' not in out
57+
58+
59+
@pytest.mark.parametrize('package', [
60+
get_tested_mock_package(files={
61+
'/var/lib/pipewire/ghost_file': {'metadata': {'flags': rpm.RPMFILE_GHOST}},
62+
}),
63+
])
64+
def test_not_atomic_ghost(package, output, test):
65+
test.check(package)
66+
out = output.print_results(output.results)
67+
assert 'W: ghost-outside-snapshot' in out

0 commit comments

Comments
 (0)