Skip to content

Commit 7503c64

Browse files
authored
Merge pull request #149 from pebenito/rokmods
checker: Implement module for asserting kernel modules are read-only.
2 parents 7e56fa0 + 57c2011 commit 7503c64

File tree

8 files changed

+408
-2
lines changed

8 files changed

+408
-2
lines changed

man/sechecker.1

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,24 @@ A space-separated list of types and type attributes. Rules with these
176176
as the source will be ignored if they allow file write or append permissions
177177
on types determined executable. This is optional.
178178

179+
.SH "READ-ONLY KERNEL MODULES ASSERTION"
180+
This checks that all file types that are for kernel modules are read-only.
181+
The check_type is \fBro_kmods\fR.
182+
183+
.PP
184+
Options:
185+
.IP "exempt_file = <type or type attribute>[ ....]"
186+
A space-separated list of types and type attributes. These
187+
will not be considered kernel modules. This is optional.
188+
.IP "exempt_load_domain = <type or type attribute>[ ....]"
189+
A space-separated list of types and type attributes. Rules with these
190+
as the source will be ignored if they allow kernel module loading permission.
191+
This is optional.
192+
.IP "exempt_write_domain = <type or type attribute>[ ....]"
193+
A space-separated list of types and type attributes. Rules with these
194+
as the source will be ignored if they allow file write or append permissions
195+
on types determined to be kernel modules. This is optional.
196+
179197
.SH "CONFIGURATION EXAMPLES"
180198

181199
.PP

setools/checker/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
from . import assertte
88
from . import emptyattr
99
from . import roexec
10+
from . import rokmod
1011

1112
from .checker import PolicyChecker

setools/checker/rokmod.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Copyright 2020, 2025, Microsoft Corporation
2+
#
3+
# SPDX-License-Identifier: LGPL-2.1-only
4+
#
5+
6+
from collections import defaultdict
7+
import typing
8+
9+
from .. import policyrep
10+
from ..terulequery import TERuleQuery
11+
12+
from .checkermodule import CheckerModule
13+
from .descriptors import ConfigSetDescriptor
14+
15+
EXEMPT_WRITE: typing.Final[str] = "exempt_write_domain"
16+
EXEMPT_LOAD: typing.Final[str] = "exempt_load_domain"
17+
EXEMPT_FILE: typing.Final[str] = "exempt_file"
18+
19+
__all__: typing.Final[tuple[str, ...]] = ("ReadOnlyKernelModules",)
20+
21+
22+
class ReadOnlyKernelModules(CheckerModule):
23+
24+
"""Checker module for asserting all kernel modules are read-only."""
25+
26+
check_type = "ro_kmods"
27+
check_config = frozenset((EXEMPT_WRITE, EXEMPT_LOAD, EXEMPT_FILE))
28+
29+
exempt_write_domain = ConfigSetDescriptor[policyrep.Type](
30+
"lookup_type_or_attr", strict=False, expand=True)
31+
exempt_file = ConfigSetDescriptor[policyrep.Type](
32+
"lookup_type_or_attr", strict=False, expand=True)
33+
exempt_load_domain = ConfigSetDescriptor[policyrep.Type](
34+
"lookup_type_or_attr", strict=False, expand=True)
35+
36+
def __init__(self, policy: policyrep.SELinuxPolicy, checkname: str,
37+
config: dict[str, str]) -> None:
38+
39+
super().__init__(policy, checkname, config)
40+
self.exempt_write_domain = config.get(EXEMPT_WRITE)
41+
self.exempt_file = config.get(EXEMPT_FILE)
42+
self.exempt_load_domain = config.get(EXEMPT_LOAD)
43+
44+
def _collect_kernel_mods(self) -> defaultdict[policyrep.Type, set[policyrep.AVRule]]:
45+
self.log.debug("Collecting list of kernel module types.")
46+
self.log.debug(f"{self.exempt_load_domain=}")
47+
query = TERuleQuery(self.policy,
48+
ruletype=("allow",),
49+
tclass=("system",),
50+
perms=("module_load",))
51+
52+
collected = defaultdict[policyrep.Type, set[policyrep.AVRule]](set)
53+
for rule in query.results():
54+
sources = set(rule.source.expand()) - self.exempt_load_domain
55+
targets = set(rule.target.expand()) - self.exempt_file
56+
57+
# remove self rules
58+
targets -= sources
59+
60+
# ignore rule if source or target is an empty attr
61+
if not sources or not targets:
62+
self.log.debug(f"Ignoring empty module_load rule: {rule}")
63+
continue
64+
65+
for t in targets:
66+
self.log.debug(f"Determined {t} is a kernel module by: {rule}")
67+
assert isinstance(rule, policyrep.AVRule), \
68+
f"Expected AVRule, got {type(rule)}, this is an SETools bug."
69+
collected[t].add(rule)
70+
71+
return collected
72+
73+
def run(self) -> list[policyrep.Type]:
74+
self.log.info("Checking kernel modules are read-only.")
75+
76+
query = TERuleQuery(self.policy,
77+
ruletype=("allow",),
78+
tclass=("file",),
79+
perms=("write", "append"))
80+
kmods = self._collect_kernel_mods()
81+
failures = defaultdict(set)
82+
83+
for kmod_type in kmods.keys():
84+
self.log.debug(f"Checking if kernel module type {kmod_type} is writable.")
85+
86+
query.target = kmod_type
87+
for rule in sorted(query.results()):
88+
if set(rule.source.expand()) - self.exempt_write_domain:
89+
failures[kmod_type].add(rule)
90+
91+
for kmod_type in sorted(failures.keys()):
92+
self.output.write("\n------------\n\n")
93+
self.output.write(f"Kernel module type {kmod_type} is writable.\n\n")
94+
self.output.write("Module load rules:\n")
95+
for rule in sorted(kmods[kmod_type]):
96+
self.output.write(f" * {rule}\n")
97+
98+
self.output.write("\nWrite rules:\n")
99+
for rule in sorted(failures[kmod_type]):
100+
self.log_fail(str(rule))
101+
102+
self.log.debug(f"{len(failures)} failure(s)")
103+
return sorted(failures.keys())

tests/library/checker/checker-valid.ini

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,10 @@ exempt_write_domain = domain1
1313
[assertte]
1414
check_type = assert_te
1515
perms = null
16+
17+
[rokmod]
18+
desc = read only kernel modules test
19+
check_type = ro_kmods
20+
exempt_load_domain = unconfined
21+
exempt_write_domain = domain1
22+
domain2 unconfined

tests/library/checker/checker.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class infoflow5
66
class infoflow6
77
class infoflow7
88
class file
9+
class system
910

1011
sid kernel
1112
sid security
@@ -63,6 +64,11 @@ class file
6364
execute_no_trans
6465
}
6566

67+
class system
68+
{
69+
module_load
70+
}
71+
6672
sensitivity low_s;
6773
sensitivity medium_s alias med;
6874
sensitivity high_s;

tests/library/checker/rokmod.conf

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
class infoflow
2+
class infoflow2
3+
class infoflow3
4+
class infoflow4
5+
class infoflow5
6+
class infoflow6
7+
class infoflow7
8+
class file
9+
class system
10+
11+
sid kernel
12+
sid security
13+
14+
common infoflow
15+
{
16+
low_w
17+
med_w
18+
hi_w
19+
low_r
20+
med_r
21+
hi_r
22+
}
23+
24+
class infoflow
25+
inherits infoflow
26+
27+
class infoflow2
28+
inherits infoflow
29+
{
30+
super_w
31+
super_r
32+
}
33+
34+
class infoflow3
35+
{
36+
null
37+
}
38+
39+
class infoflow4
40+
inherits infoflow
41+
42+
class infoflow5
43+
inherits infoflow
44+
45+
class infoflow6
46+
inherits infoflow
47+
48+
class infoflow7
49+
inherits infoflow
50+
{
51+
super_w
52+
super_r
53+
super_none
54+
super_both
55+
super_unmapped
56+
}
57+
58+
class file
59+
{
60+
read
61+
write
62+
append
63+
execute
64+
execute_no_trans
65+
}
66+
67+
class system
68+
{
69+
module_load
70+
}
71+
72+
sensitivity low_s;
73+
sensitivity medium_s alias med;
74+
sensitivity high_s;
75+
76+
dominance { low_s med high_s }
77+
78+
category here;
79+
category there;
80+
category elsewhere alias lost;
81+
82+
#level decl
83+
level low_s:here.there;
84+
level med:here, elsewhere;
85+
level high_s:here.lost;
86+
87+
#some constraints
88+
mlsconstrain infoflow hi_r ((l1 dom l2) or (t1 == mls_exempt));
89+
90+
attribute mls_exempt;
91+
92+
type system;
93+
role system;
94+
role system types system;
95+
96+
################################################################################
97+
# Type enforcement declarations and rules
98+
99+
attribute domain;
100+
type domain1, domain;
101+
type domain2, domain;
102+
type unconfined, domain;
103+
104+
attribute empty_source_attr;
105+
attribute empty_target_attr;
106+
107+
attribute files;
108+
attribute exempt_files_attr;
109+
type exempt_file, files;
110+
type kmodfile1, files, exempt_files_attr;
111+
type kmodfile2, files, exempt_files_attr;
112+
113+
# Baseline read-only kmod:
114+
type rokmod, files;
115+
allow domain self:system module_load;
116+
allow domain rokmod:system module_load;
117+
118+
# Baseline non-module file (kmod due to unconfined):
119+
type nonkmod, files;
120+
allow domain nonkmod:file read;
121+
122+
# Unconfined
123+
allow unconfined self:system module_load;
124+
allow unconfined files:system module_load;
125+
126+
# writer
127+
allow domain1 kmodfile1:file write;
128+
allow domain2 self:system module_load;
129+
allow domain2 kmodfile1:system module_load;
130+
131+
# appender
132+
allow domain2 kmodfile2:file append;
133+
allow domain1 self:system module_load;
134+
allow domain1 kmodfile2:system module_load;
135+
136+
# empty attrs
137+
allow empty_source_attr self:system module_load;
138+
allow empty_source_attr nonkmod:file { read write append };
139+
allow empty_source_attr nonkmod:system module_load;
140+
allow domain1 self:system module_load;
141+
allow domain1 empty_target_attr:file { read write append };
142+
allow domain1 empty_target_attr:system module_load;
143+
144+
################################################################################
145+
146+
#users
147+
user system roles system level med range low_s - high_s:here.lost;
148+
149+
#normal constraints
150+
constrain infoflow hi_w (u1 == u2);
151+
152+
#isids
153+
sid kernel system:system:system:medium_s:here
154+
sid security system:system:system:high_s:lost
155+
156+
#fs_use
157+
fs_use_trans devpts system:object_r:system:low_s;
158+
fs_use_xattr ext3 system:object_r:system:low_s;
159+
fs_use_task pipefs system:object_r:system:low_s;
160+
161+
#genfscon
162+
genfscon proc / system:object_r:system:med
163+
genfscon proc /sys system:object_r:system:low_s
164+
genfscon selinuxfs / system:object_r:system:high_s:here.there
165+
166+
portcon tcp 80 system:object_r:system:low_s
167+
168+
netifcon eth0 system:object_r:system:low_s system:object_r:system:low_s
169+
170+
nodecon 127.0.0.1 255.255.255.255 system:object_r:system:low_s:here
171+
nodecon ::1 ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff system:object_r:system:low_s:here
172+

tests/library/checker/test_checker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def test_run_pass(self, compiled_policy: setools.SELinuxPolicy) -> None:
5555
newcheck.run.return_value = []
5656
checker.checks.append(newcheck)
5757

58-
assert 4 == len(checker.checks)
58+
assert 5 == len(checker.checks)
5959
result = checker.run(output=fd)
6060
assert 0 == result
6161
newcheck.run.assert_not_called()
@@ -74,7 +74,7 @@ def test_run_fail(self, compiled_policy: setools.SELinuxPolicy) -> None:
7474
newcheck.run.return_value = list(range(13))
7575
checker.checks.append(newcheck)
7676

77-
assert 4 == len(checker.checks)
77+
assert 5 == len(checker.checks)
7878

7979
result = checker.run(output=fd)
8080
newcheck.run.assert_called()

0 commit comments

Comments
 (0)