commit 3dbf6af73884cc2b822917720b3f87c0128cd300 Author: Pavel 'LEdoian' Turinsky Date: Fri Jul 8 09:44:42 2022 +0200 Initial version Proof-of-concept quality, but works, somewhat. diff --git a/chktaint.py b/chktaint.py new file mode 100755 index 0000000..8cce52b --- /dev/null +++ b/chktaint.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 + +from dataclasses import dataclass +import argparse +from sys import argv, exit, stderr +from copy import copy +import pathlib +import formats + +# Table from https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html: +_taint_table = [ + "0 G/P 1 proprietary module was loaded", + "1 _/F 2 module was force loaded", + "2 _/S 4 kernel running on an out of specification system", + "3 _/R 8 module was force unloaded", + "4 _/M 16 processor reported a Machine Check Exception (MCE)", + "5 _/B 32 bad page referenced or some unexpected page flags", + "6 _/U 64 taint requested by userspace application", + "7 _/D 128 kernel died recently, i.e. there was an OOPS or BUG", + "8 _/A 256 ACPI table overridden by user", + "9 _/W 512 kernel issued warning", + "10 _/C 1024 staging driver was loaded", + "11 _/I 2048 workaround for bug in platform firmware applied", + "12 _/O 4096 externally-built (“out-of-tree”) module was loaded", + "13 _/E 8192 unsigned module was loaded", + "14 _/L 16384 soft lockup occurred", + "15 _/K 32768 kernel has been live patched", + "16 _/X 65536 auxiliary taint, defined for and used by distros", + "17 _/T 131072 kernel was built with the struct randomization plugin", + ] + +# FIXME: Support for multi-bit taints? (i.e. P+O=4097 means likely weird driver…) +@dataclass +class TaintBit: + position: int + true_letter: str # Taint set + false_letter: str # Taint not set + description: str + + def __str__(self): return self.description + f' (bit {self.position})' + + +class TaintTable: + def __init__(self, bits: dict[int, TaintBit]): + self.bits = bits + # Pre-compute the all-false string + self.false_letters = ['' for _ in range(max(self.bits.keys())+1)] + for bit in self.bits.values(): + self.false_letters[bit.position] = bit.false_letter + + def get_taints(self, value): + # FIXME: might be slow? + result = [] + l = value.bit_length() + for pos,bit in self.bits.items(): + if pos >= l: continue + if value & (1 << bit.position): + result.append(bit) + return sorted(result, key=lambda b:b.position) + + def get_taint_letters(self, taints): + letters = copy(self.false_letters) + for t in taints: + letters[t.position] = t.true_letter + return ''.join(letters) + +def get_taint_mapping(table): + result = {} + for line in table: + pos, letters, _dec, descr = line.split(' ', maxsplit=3) + pos = int(pos) + falselet, truelet = letters.split('/') + if falselet == '_': falselet = ' ' + assert pos not in result + result[pos] = TaintBit(position=pos, true_letter=truelet, false_letter=falselet, description=descr) + return TaintTable(result) + +TAINTS = get_taint_mapping(_taint_table) + +def strict_check(taints, value) -> bool: + correct_value = sum(1<= 2: die(reason) + + collision('Can show either script- or human-friendly output, not both.', + args.script, + args.human) + collision('Can either read taint value from file, stdin, argument or print whole table, not multiple.', + args.file is not None, + args.info, + args.int is not None, + args.stdin) + if args.int is not None and args.int < 0: die('Only non-negative values are valid taint values.') + if args.file is not None and not args.file.exists(): die('The specified file does not exist.') + + ############################# + # # + # Phew, arguments parsed! # + # # + ############################# + + if args.int is not None: taint_val = args.int + elif args.file is not None: + with open(args.file, 'r') as f: + taint_val = int(f.readline()) + elif args.stdin: taint_val = int(input()) + elif args.info: + taint_val = sum(1< (script_name, formatter) + # FIXME: Too much boilerplate + + output_data = { + 'descriptions': ('DESCRIPTIONS', formats.description_format), + 'bits': ('BITS', formats.bit_format), + 'letters': ('LETTERS', formats.letter_format), + 'chktaint': ('CHKTAINT', formats.chktaint_format), + } + + default_contents = not any([args.quiet, args.descriptions, args.letters, args.bits, args.chktaint]) + default_format = not any([args.script, args.human]) + + # Set defaults + if default_format: args.human = True + if default_contents and args.human: args.chktaint = True + if default_contents and args.script: args.letters = True + + for cont in ['descriptions', 'bits', 'letters', 'chktaint']: + if getattr(args, cont): + output = [] + if args.script: output.append(output_data[cont][0]+'=') + output.append(output_data[cont][1](taints, TAINTS)) + print(''.join(output)) + + + return_value = 1 if taints else 0 + if args.strict: return_value = 2 if not strict_check(taints, taint_val) else return_value + # FIXME: Output when strict and not quiet? + + exit(return_value) + + + + + +if __name__ == '__main__': main() diff --git a/formats.py b/formats.py new file mode 100644 index 0000000..5dfc444 --- /dev/null +++ b/formats.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +""" +Helper file to define all the possible formats in. + +All formats get an iterable of taints and the taint table and output a single string of result. +""" + +def chktaint_format(taints, _table): + _chktaint_fmt = ' * {taint.description} (#{taint.position})' + if taints: + # TODO: Prolog and epilog? + result = [_chktaint_fmt.format(taint=t) for t in taints] + else: + result = ['Kernel not tainted'] + return '\n'.join(result) + +def bit_format(taints, _table): + return ','.join(str(t.position) for t in taints) + +def letter_format(taints, table): + return '\''+table.get_taint_letters(taints)+'\'' + +def description_format(taints, _table): + return '\n'.join(t.description for t in taints)