vuls: init at 0.27.0
[NixPkgs.git] / nixos / tests / systemd-confinement / checkperms.py
blob3c7ba279a3d20f161c809fc05c731605334245d4
1 import errno
2 import os
4 from enum import IntEnum
5 from pathlib import Path
8 class Accessibility(IntEnum):
9 """
10 The level of accessibility we have on a file or directory.
12 This is needed to assess the attack surface on the file system namespace we
13 have within a confined service. Higher levels mean more permissions for the
14 user and thus a bigger attack surface.
15 """
16 NONE = 0
18 # Directories can be listed or files can be read.
19 READABLE = 1
21 # This is for special file systems such as procfs and for stuff such as
22 # FIFOs or character special files. The reason why this has a lower value
23 # than WRITABLE is because those files are more restricted on what and how
24 # they can be written to.
25 SPECIAL = 2
27 # Another special case are sticky directories, which do allow write access
28 # but restrict deletion. This does *not* apply to sticky directories that
29 # are read-only.
30 STICKY = 3
32 # Essentially full permissions, the kind of accessibility we want to avoid
33 # in most cases.
34 WRITABLE = 4
36 def assert_on(self, path: Path) -> None:
37 """
38 Raise an AssertionError if the given 'path' allows for more
39 accessibility than 'self'.
40 """
41 actual = self.NONE
43 if path.is_symlink():
44 actual = self.READABLE
45 elif path.is_dir():
46 writable = True
48 dummy_file = path / 'can_i_write'
49 try:
50 dummy_file.touch()
51 except OSError as e:
52 if e.errno in [errno.EROFS, errno.EACCES]:
53 writable = False
54 else:
55 raise
56 else:
57 dummy_file.unlink()
59 if writable:
60 # The reason why we test this *after* we made sure it's
61 # writable is because we could have a sticky directory where
62 # the current user doesn't have write access.
63 if path.stat().st_mode & 0o1000 == 0o1000:
64 actual = self.STICKY
65 else:
66 actual = self.WRITABLE
67 else:
68 actual = self.READABLE
69 elif path.is_file():
70 try:
71 with path.open('rb') as fp:
72 fp.read(1)
73 actual = self.READABLE
74 except PermissionError:
75 pass
77 writable = True
78 try:
79 with path.open('ab') as fp:
80 fp.write('x')
81 size = fp.tell()
82 fp.truncate(size)
83 except PermissionError:
84 writable = False
85 except OSError as e:
86 if e.errno == errno.ETXTBSY:
87 writable = os.access(path, os.W_OK)
88 elif e.errno == errno.EROFS:
89 writable = False
90 else:
91 raise
93 # Let's always try to fail towards being writable, so if *either*
94 # access(2) or a real write is successful it's writable. This is to
95 # make sure we don't accidentally introduce no-ops if we have bugs
96 # in the more complicated real write code above.
97 if writable or os.access(path, os.W_OK):
98 actual = self.WRITABLE
99 else:
100 # We need to be very careful when writing to or reading from
101 # special files (eg. FIFOs), since they can possibly block. So if
102 # it's not a file, just trust that access(2) won't lie.
103 if os.access(path, os.R_OK):
104 actual = self.READABLE
106 if os.access(path, os.W_OK):
107 actual = self.SPECIAL
109 if actual > self:
110 stat = path.stat()
111 details = ', '.join([
112 f'permissions: {stat.st_mode & 0o7777:o}',
113 f'uid: {stat.st_uid}',
114 f'group: {stat.st_gid}',
117 raise AssertionError(
118 f'Expected at most {self!r} but got {actual!r} for path'
119 f' {path} ({details}).'
123 def is_special_fs(path: Path) -> bool:
125 Check whether the given path truly is a special file system such as procfs
126 or sysfs.
128 try:
129 if path == Path('/proc'):
130 return (path / 'version').read_text().startswith('Linux')
131 elif path == Path('/sys'):
132 return b'Linux' in (path / 'kernel' / 'notes').read_bytes()
133 except FileNotFoundError:
134 pass
135 return False
138 def is_empty_dir(path: Path) -> bool:
139 try:
140 next(path.iterdir())
141 return False
142 except (StopIteration, PermissionError):
143 return True
146 def _assert_permissions_in_directory(
147 directory: Path,
148 accessibility: Accessibility,
149 subdirs: dict[Path, Accessibility],
150 ) -> None:
151 accessibility.assert_on(directory)
153 for file in directory.iterdir():
154 if is_special_fs(file):
155 msg = f'Got unexpected special filesystem at {file}.'
156 assert subdirs.pop(file) == Accessibility.SPECIAL, msg
157 elif not file.is_symlink() and file.is_dir():
158 subdir_access = subdirs.pop(file, accessibility)
159 if is_empty_dir(file):
160 # Whenever we got an empty directory, we check the permission
161 # constraints on the current directory (except if specified
162 # explicitly in subdirs) because for example if we're non-root
163 # (the constraints of the current directory are thus
164 # Accessibility.READABLE), we really have to make sure that
165 # empty directories are *never* writable.
166 subdir_access.assert_on(file)
167 else:
168 _assert_permissions_in_directory(file, subdir_access, subdirs)
169 else:
170 subdirs.pop(file, accessibility).assert_on(file)
173 def assert_permissions(subdirs: dict[str, Accessibility]) -> None:
175 Recursively check whether the file system conforms to the accessibility
176 specification we specified via 'subdirs'.
178 root = Path('/')
179 absolute_subdirs = {root / p: a for p, a in subdirs.items()}
180 _assert_permissions_in_directory(
181 root,
182 Accessibility.WRITABLE if os.getuid() == 0 else Accessibility.READABLE,
183 absolute_subdirs,
185 for file in absolute_subdirs.keys():
186 msg = f'Expected {file} to exist, but it was nowwhere to be found.'
187 raise AssertionError(msg)