Disabling X86_MSR is recommended by KSPP
[kconfig-hardened-check.git] / kconfig_hardened_check / __init__.py
index 35731e38366d54231303d8954db3096de82af640..fb9d37be3e75be1b0b0b693794789ada842215b1 100644 (file)
@@ -14,6 +14,8 @@
 #    slab_nomerge
 #    page_alloc.shuffle=1
 #    iommu=force (does it help against DMA attacks?)
+#    iommu.passthrough=0
+#    iommu.strict=1
 #    slub_debug=FZ (slow)
 #    init_on_alloc=1 (since v5.3)
 #    init_on_free=1 (since v5.3, otherwise slub_debug=P and page_poison=1)
@@ -29,9 +31,9 @@
 #           pti=on
 #           spec_store_bypass_disable=on
 #           l1tf=full,force
+#           l1d_flush=on (a part of the l1tf option)
 #           mds=full,nosmt
 #           tsx=off
-#           l1d_flush=on
 #       ARM64:
 #           kpti=on
 #           ssbd=force-on
@@ -84,7 +86,10 @@ from .__about__ import __version__
 TYPES_OF_CHECKS = ('kconfig', 'version')
 
 class OptCheck:
-    def __init__(self, reason, decision, name, expected):
+    # Constructor without the 'expected' parameter is for option presence checks (any value is OK)
+    def __init__(self, reason, decision, name, expected=None):
+        if not reason or not decision or not name:
+            sys.exit('[!] ERROR: invalid {} check for "{}"'.format(self.__class__.__name__, name))
         self.name = name
         self.expected = expected
         self.decision = decision
@@ -93,6 +98,15 @@ class OptCheck:
         self.result = None
 
     def check(self):
+        # handle the option presence check
+        if self.expected is None:
+            if self.state is None:
+                self.result = 'FAIL: not present'
+            else:
+                self.result = 'OK: is present'
+            return
+
+        # handle the option value check
         if self.expected == self.state:
             self.result = 'OK'
         elif self.state is None:
@@ -103,12 +117,12 @@ class OptCheck:
         else:
             self.result = 'FAIL: "' + self.state + '"'
 
-        if self.result.startswith('OK'):
-            return True
-        return False
-
     def table_print(self, _mode, with_results):
-        print('{:<40}|{:^7}|{:^12}|{:^10}|{:^18}'.format(self.name, self.type, self.expected, self.decision, self.reason), end='')
+        if self.expected is None:
+            expected = ''
+        else:
+            expected = self.expected
+        print('{:<40}|{:^7}|{:^12}|{:^10}|{:^18}'.format(self.name, self.type, expected, self.decision, self.reason), end='')
         if with_results:
             print('| {}'.format(self.result), end='')
 
@@ -142,15 +156,14 @@ class VersionCheck:
     def check(self):
         if self.ver[0] > self.ver_expected[0]:
             self.result = 'OK: version >= ' + str(self.ver_expected[0]) + '.' + str(self.ver_expected[1])
-            return True
+            return
         if self.ver[0] < self.ver_expected[0]:
             self.result = 'FAIL: version < ' + str(self.ver_expected[0]) + '.' + str(self.ver_expected[1])
-            return False
+            return
         if self.ver[1] >= self.ver_expected[1]:
             self.result = 'OK: version >= ' + str(self.ver_expected[0]) + '.' + str(self.ver_expected[1])
-            return True
+            return
         self.result = 'FAIL: version < ' + str(self.ver_expected[0]) + '.' + str(self.ver_expected[1])
-        return False
 
     def table_print(self, _mode, with_results):
         ver_req = 'kernel version >= ' + str(self.ver_expected[0]) + '.' + str(self.ver_expected[1])
@@ -159,29 +172,6 @@ class VersionCheck:
             print('| {}'.format(self.result), end='')
 
 
-class PresenceCheck:
-    def __init__(self, name, type):
-        self.type = type
-        if self.type == 'kconfig':
-            self.name = 'CONFIG_' + name
-        else:
-            sys.exit('[!] ERROR: unsupported type "{}" for {}'.format(type, self.__class__.__name__))
-        self.state = None
-        self.result = None
-
-    def check(self):
-        if self.state is None:
-            self.result = 'FAIL: not present'
-            return False
-        self.result = 'OK: is present'
-        return True
-
-    def table_print(self, _mode, with_results):
-        print('{:<91}'.format(self.name + ' is present'), end='')
-        if with_results:
-            print('| {}'.format(self.result), end='')
-
-
 class ComplexOptCheck:
     def __init__(self, *opts):
         self.opts = opts
@@ -239,22 +229,26 @@ class OR(ComplexOptCheck):
     # Use cases:
     #     OR(<X_is_hardened>, <X_is_disabled>)
     #     OR(<X_is_hardened>, <old_X_is_hardened>)
-
     def check(self):
         if not self.opts:
             sys.exit('[!] ERROR: invalid OR check')
-
         for i, opt in enumerate(self.opts):
-            ret = opt.check()
-            if ret:
-                if opt.result == 'OK' and i != 0:
-                    # Simple OK is not enough for additional checks, add more info:
-                    self.result = 'OK: {} "{}"'.format(opt.name, opt.expected)
-                else:
-                    self.result = opt.result
-                return True
+            opt.check()
+            if opt.result.startswith('OK'):
+                self.result = opt.result
+                # Add more info for additional checks:
+                if i != 0:
+                    if opt.result == 'OK':
+                        self.result = 'OK: {} "{}"'.format(opt.name, opt.expected)
+                    elif opt.result == 'OK: not found':
+                        self.result = 'OK: {} not found'.format(opt.name)
+                    elif opt.result == 'OK: is present':
+                        self.result = 'OK: {} is present'.format(opt.name)
+                    # VersionCheck provides enough info
+                    elif not opt.result.startswith('OK: version'):
+                        sys.exit('[!] ERROR: unexpected OK description "{}"'.format(opt.result))
+                return
         self.result = self.opts[0].result
-        return False
 
 
 class AND(ComplexOptCheck):
@@ -263,14 +257,13 @@ class AND(ComplexOptCheck):
     #     AND(<suboption>, <main_option>)
     #       Suboption is not checked if checking of the main_option is failed.
     #     AND(<X_is_disabled>, <old_X_is_disabled>)
-
     def check(self):
         for i, opt in reversed(list(enumerate(self.opts))):
-            ret = opt.check()
+            opt.check()
             if i == 0:
                 self.result = opt.result
-                return ret
-            if not ret:
+                return
+            if not opt.result.startswith('OK'):
                 # This FAIL is caused by additional checks,
                 # and not by the main option that this AND-check is about.
                 # Describe the reason of the FAIL.
@@ -279,10 +272,11 @@ class AND(ComplexOptCheck):
                 elif opt.result == 'FAIL: not present':
                     self.result = 'FAIL: {} not present'.format(opt.name)
                 else:
-                    # This FAIL message is self-explaining.
+                    # VersionCheck provides enough info
                     self.result = opt.result
-                return False
-
+                    if not opt.result.startswith('FAIL: version'):
+                        sys.exit('[!] ERROR: unexpected FAIL description "{}"'.format(opt.result))
+                return
         sys.exit('[!] ERROR: invalid AND check')
 
 
@@ -325,6 +319,8 @@ def add_kconfig_checks(l, arch):
 
     modules_not_set = KconfigCheck('cut_attack_surface', 'kspp', 'MODULES', 'is not set')
     devmem_not_set = KconfigCheck('cut_attack_surface', 'kspp', 'DEVMEM', 'is not set') # refers to LOCKDOWN
+    bpf_syscall_not_set = KconfigCheck('cut_attack_surface', 'lockdown', 'BPF_SYSCALL', 'is not set') # refers to LOCKDOWN
+    efi_not_set = KconfigCheck('cut_attack_surface', 'my', 'EFI', 'is not set')
 
     # 'self_protection', 'defconfig'
     l += [KconfigCheck('self_protection', 'defconfig', 'BUG', 'y')]
@@ -339,11 +335,11 @@ def add_kconfig_checks(l, arch):
              modules_not_set)] # DEBUG_SET_MODULE_RONX was before v4.11
     l += [OR(KconfigCheck('self_protection', 'defconfig', 'REFCOUNT_FULL', 'y'),
              VersionCheck((5, 5)))] # REFCOUNT_FULL is enabled by default since v5.5
+    l += [KconfigCheck('self_protection', 'defconfig', 'THREAD_INFO_IN_TASK', 'y')]
     iommu_support_is_set = KconfigCheck('self_protection', 'defconfig', 'IOMMU_SUPPORT', 'y')
     l += [iommu_support_is_set] # is needed for mitigating DMA attacks
     if arch in ('X86_64', 'ARM64', 'X86_32'):
         l += [KconfigCheck('self_protection', 'defconfig', 'RANDOMIZE_BASE', 'y')]
-        l += [KconfigCheck('self_protection', 'defconfig', 'THREAD_INFO_IN_TASK', 'y')]
     if arch in ('X86_64', 'ARM64'):
         l += [KconfigCheck('self_protection', 'defconfig', 'VMAP_STACK', 'y')]
     if arch in ('X86_64', 'X86_32'):
@@ -374,10 +370,12 @@ def add_kconfig_checks(l, arch):
         l += [KconfigCheck('self_protection', 'defconfig', 'ARM64_BTI_KERNEL', 'y')]
         l += [OR(KconfigCheck('self_protection', 'defconfig', 'HARDEN_BRANCH_PREDICTOR', 'y'),
                  VersionCheck((5, 10)))] # HARDEN_BRANCH_PREDICTOR is enabled by default since v5.10
+        l += [KconfigCheck('self_protection', 'defconfig', 'MITIGATE_SPECTRE_BRANCH_HISTORY', 'y')]
         l += [KconfigCheck('self_protection', 'defconfig', 'ARM64_MTE', 'y')]
     if arch == 'ARM':
         l += [KconfigCheck('self_protection', 'defconfig', 'CPU_SW_DOMAIN_PAN', 'y')]
         l += [KconfigCheck('self_protection', 'defconfig', 'HARDEN_BRANCH_PREDICTOR', 'y')]
+        l += [KconfigCheck('self_protection', 'defconfig', 'HARDEN_BRANCH_HISTORY', 'y')]
 
     # 'self_protection', 'kspp'
     l += [KconfigCheck('self_protection', 'kspp', 'SECURITY_DMESG_RESTRICT', 'y')]
@@ -394,6 +392,9 @@ def add_kconfig_checks(l, arch):
     l += [KconfigCheck('self_protection', 'kspp', 'DEBUG_NOTIFIERS', 'y')]
     l += [KconfigCheck('self_protection', 'kspp', 'INIT_ON_ALLOC_DEFAULT_ON', 'y')]
     l += [KconfigCheck('self_protection', 'kspp', 'GCC_PLUGIN_LATENT_ENTROPY', 'y')]
+    l += [KconfigCheck('self_protection', 'kspp', 'KFENCE', 'y')]
+    l += [KconfigCheck('self_protection', 'kspp', 'WERROR', 'y')]
+    l += [KconfigCheck('self_protection', 'kspp', 'IOMMU_DEFAULT_DMA_STRICT', 'y')]
     randstruct_is_set = KconfigCheck('self_protection', 'kspp', 'GCC_PLUGIN_RANDSTRUCT', 'y')
     l += [randstruct_is_set]
     hardened_usercopy_is_set = KconfigCheck('self_protection', 'kspp', 'HARDENED_USERCOPY', 'y')
@@ -423,6 +424,7 @@ def add_kconfig_checks(l, arch):
         stackleak_is_set = KconfigCheck('self_protection', 'kspp', 'GCC_PLUGIN_STACKLEAK', 'y')
         l += [stackleak_is_set]
         l += [KconfigCheck('self_protection', 'kspp', 'RANDOMIZE_KSTACK_OFFSET_DEFAULT', 'y')]
+        l += [KconfigCheck('self_protection', 'kspp', 'SCHED_CORE', 'y')]
     if arch in ('X86_64', 'X86_32'):
         l += [KconfigCheck('self_protection', 'kspp', 'DEFAULT_MMAP_MIN_ADDR', '65536')]
     if arch in ('ARM64', 'ARM'):
@@ -438,15 +440,17 @@ def add_kconfig_checks(l, arch):
     # 'self_protection', 'maintainer'
     ubsan_bounds_is_set = KconfigCheck('self_protection', 'maintainer', 'UBSAN_BOUNDS', 'y') # only array index bounds checking
     l += [ubsan_bounds_is_set] # recommended by Kees Cook in /issues/53
-    l += [AND(KconfigCheck('self_protection', 'maintainer', 'UBSAN_SANITIZE_ALL', 'y'),
-              ubsan_bounds_is_set)] # recommended by Kees Cook in /issues/53
+    if arch in ('X86_64', 'ARM64', 'X86_32'):  # ARCH_HAS_UBSAN_SANITIZE_ALL is not enabled for ARM
+        l += [AND(KconfigCheck('self_protection', 'maintainer', 'UBSAN_SANITIZE_ALL', 'y'),
+                  ubsan_bounds_is_set)] # recommended by Kees Cook in /issues/53
     l += [AND(KconfigCheck('self_protection', 'maintainer', 'UBSAN_TRAP', 'y'),
               ubsan_bounds_is_set)] # recommended by Kees Cook in /issues/53
 
     # 'self_protection', 'clipos'
     l += [KconfigCheck('self_protection', 'clipos', 'DEBUG_VIRTUAL', 'y')]
     l += [KconfigCheck('self_protection', 'clipos', 'STATIC_USERMODEHELPER', 'y')] # needs userspace support
-    l += [KconfigCheck('self_protection', 'clipos', 'EFI_DISABLE_PCI_DMA', 'y')]
+    l += [OR(KconfigCheck('self_protection', 'clipos', 'EFI_DISABLE_PCI_DMA', 'y'),
+             efi_not_set)]
     l += [KconfigCheck('self_protection', 'clipos', 'SLAB_MERGE_DEFAULT', 'is not set')] # slab_nomerge
     l += [KconfigCheck('self_protection', 'clipos', 'RANDOM_TRUST_BOOTLOADER', 'is not set')]
     l += [KconfigCheck('self_protection', 'clipos', 'RANDOM_TRUST_CPU', 'is not set')]
@@ -468,8 +472,10 @@ def add_kconfig_checks(l, arch):
                   iommu_support_is_set)]
 
     # 'self_protection', 'my'
-    l += [KconfigCheck('self_protection', 'my', 'RESET_ATTACK_MITIGATION', 'y')] # needs userspace support (systemd)
+    l += [OR(KconfigCheck('self_protection', 'my', 'RESET_ATTACK_MITIGATION', 'y'),
+             efi_not_set)] # needs userspace support (systemd)
     if arch == 'X86_64':
+        l += [KconfigCheck('self_protection', 'my', 'SLS', 'y')] # vs CVE-2021-26341 in Straight-Line-Speculation
         l += [AND(KconfigCheck('self_protection', 'my', 'AMD_IOMMU_V2', 'y'),
                   iommu_support_is_set)]
     if arch == 'ARM64':
@@ -498,6 +504,8 @@ def add_kconfig_checks(l, arch):
               loadpin_is_set)]
 
     # 'cut_attack_surface', 'defconfig'
+    l += [OR(KconfigCheck('cut_attack_surface', 'defconfig', 'BPF_UNPRIV_DEFAULT_OFF', 'y'),
+             bpf_syscall_not_set)] # see unprivileged_bpf_disabled
     l += [KconfigCheck('cut_attack_surface', 'defconfig', 'SECCOMP', 'y')]
     l += [KconfigCheck('cut_attack_surface', 'defconfig', 'SECCOMP_FILTER', 'y')]
     if arch in ('X86_64', 'ARM64', 'X86_32'):
@@ -519,6 +527,7 @@ def add_kconfig_checks(l, arch):
     l += [KconfigCheck('cut_attack_surface', 'kspp', 'X86_X32', 'is not set')]
     l += [KconfigCheck('cut_attack_surface', 'kspp', 'MODIFY_LDT_SYSCALL', 'is not set')]
     l += [KconfigCheck('cut_attack_surface', 'kspp', 'OABI_COMPAT', 'is not set')]
+    l += [KconfigCheck('cut_attack_surface', 'kspp', 'X86_MSR', 'is not set')] # refers to LOCKDOWN
     l += [modules_not_set]
     l += [devmem_not_set]
     l += [OR(KconfigCheck('cut_attack_surface', 'kspp', 'IO_STRICT_DEVMEM', 'y'),
@@ -588,26 +597,24 @@ def add_kconfig_checks(l, arch):
     l += [KconfigCheck('cut_attack_surface', 'clipos', 'MAGIC_SYSRQ', 'is not set')]
     l += [KconfigCheck('cut_attack_surface', 'clipos', 'KEXEC_FILE', 'is not set')] # refers to LOCKDOWN (permissive)
     l += [KconfigCheck('cut_attack_surface', 'clipos', 'USER_NS', 'is not set')] # user.max_user_namespaces=0
-    l += [KconfigCheck('cut_attack_surface', 'clipos', 'X86_MSR', 'is not set')] # refers to LOCKDOWN
     l += [KconfigCheck('cut_attack_surface', 'clipos', 'X86_CPUID', 'is not set')]
     l += [KconfigCheck('cut_attack_surface', 'clipos', 'X86_IOPL_IOPERM', 'is not set')] # refers to LOCKDOWN
     l += [KconfigCheck('cut_attack_surface', 'clipos', 'ACPI_TABLE_UPGRADE', 'is not set')] # refers to LOCKDOWN
     l += [KconfigCheck('cut_attack_surface', 'clipos', 'EFI_CUSTOM_SSDT_OVERLAYS', 'is not set')]
     l += [AND(KconfigCheck('cut_attack_surface', 'clipos', 'LDISC_AUTOLOAD', 'is not set'),
-              PresenceCheck('LDISC_AUTOLOAD', 'kconfig'))]
+              KconfigCheck('cut_attack_surface', 'clipos', 'LDISC_AUTOLOAD'))] # option presence check
     if arch in ('X86_64', 'X86_32'):
         l += [KconfigCheck('cut_attack_surface', 'clipos', 'X86_INTEL_TSX_MODE_OFF', 'y')] # tsx=off
 
     # 'cut_attack_surface', 'lockdown'
+    l += [bpf_syscall_not_set] # refers to LOCKDOWN
     l += [KconfigCheck('cut_attack_surface', 'lockdown', 'EFI_TEST', 'is not set')] # refers to LOCKDOWN
-    l += [KconfigCheck('cut_attack_surface', 'lockdown', 'BPF_SYSCALL', 'is not set')] # refers to LOCKDOWN
     l += [KconfigCheck('cut_attack_surface', 'lockdown', 'MMIOTRACE_TEST', 'is not set')] # refers to LOCKDOWN
     l += [KconfigCheck('cut_attack_surface', 'lockdown', 'KPROBES', 'is not set')] # refers to LOCKDOWN
 
     # 'cut_attack_surface', 'my'
     l += [OR(KconfigCheck('cut_attack_surface', 'my', 'TRIM_UNUSED_KSYMS', 'y'),
              modules_not_set)]
-    l += [KconfigCheck('cut_attack_surface', 'my', 'BPF_UNPRIV_DEFAULT_OFF', 'y')] # see kernel.unprivileged_bpf_disabled
     l += [KconfigCheck('cut_attack_surface', 'my', 'MMIOTRACE', 'is not set')] # refers to LOCKDOWN (permissive)
     l += [KconfigCheck('cut_attack_surface', 'my', 'LIVEPATCH', 'is not set')]
     l += [KconfigCheck('cut_attack_surface', 'my', 'IP_DCCP', 'is not set')]
@@ -717,6 +724,8 @@ def populate_simple_opt_with_data(opt, data, data_type):
         opt.state = data.get(opt.name, None)
     elif data_type == 'version':
         opt.ver = data
+    else:
+        sys.exit('[!] ERROR: unexpected data type "{}"'.format(data_type))
 
 
 def populate_opt_with_data(opt, data, data_type):
@@ -766,8 +775,6 @@ def parse_kconfig_file(parsed_options, fname):
             if option:
                 parsed_options[option] = value
 
-        return parsed_options
-
 
 def main():
     # Report modes: