#!/usr/bin/env python3 # # Cert: Some extra CERT checkers # # Cppcheck itself handles many CERT rules. Cppcheck warns when there is undefined behaviour. # CERT Homepage: https://www.cert.org/secure-coding/ # # Example usage of this addon (scan a sourcefile main.cpp) # cppcheck --dump main.cpp # python cert.py main.cpp.dump import argparse from . import cppcheckdata import sys import re VERIFY = ('-verify' in sys.argv) VERIFY_EXPECTED = [] VERIFY_ACTUAL = [] def reportError(token, severity, msg, id): if VERIFY: VERIFY_ACTUAL.append(str(token.linenr) + ':cert-' + id) else: cppcheckdata.reportError(token, severity, msg, 'cert', id) def simpleMatch(token, pattern): for p in pattern.split(' '): if not token or token.str != p: return False token = token.next return True def isUnpackedStruct(token): if token.valueType is None: return False if token.valueType.typeScope is None: return False if token.valueType.typeScope.type != "Struct": return False startToken = token.valueType.typeScope.bodyStart linenr = int(startToken.linenr) for line in open(startToken.file): linenr -= 1 if linenr == 0: return True if linenr < 3 and re.match(r'#pragma\s+pack\s*\(', line): return False return True def isLocalUnpackedStruct(arg): if arg and arg.str == '&' and not arg.astOperand2: arg = arg.astOperand1 return arg and arg.variable and (arg.variable.isLocal or arg.variable.isArgument) and isUnpackedStruct(arg) def isBitwiseOp(token): return token and (token.str in {'&', '|', '^'}) def isComparisonOp(token): return token and (token.str in {'==', '!=', '>', '>=', '<', '<='}) def isCast(expr): if not expr or expr.str != '(' or not expr.astOperand1 or expr.astOperand2: return False if simpleMatch(expr, '( )'): return False return True def isStandardFunction(token): if token.function: return False prev = token.previous if prev: if prev.str == '.': return False if prev.str == '::': prevprev = prev.previous if prevprev and not prevprev.str == 'std': return False return True # Is this a function call def isFunctionCall(token, function_names, number_of_arguments=None): if not token.isName: return False if token.str not in function_names: return False if (token.next is None) or token.next.str != '(' or token.next != token.astParent: return False if number_of_arguments is None: return True return len(cppcheckdata.getArguments(token)) == number_of_arguments # EXP05-C # do not attempt to cast away const def exp05(data): # TODO Reuse code in misra rule 11.8 for token in data.tokenlist: if isCast(token): # C-style cast if not token.valueType: continue if not token.astOperand1.valueType: continue if token.valueType.pointer == 0: continue if token.astOperand1.valueType.pointer == 0: continue const1 = token.valueType.constness const2 = token.astOperand1.valueType.constness if (const1 % 2) < (const2 % 2): reportError(token, 'style', "Attempt to cast away const", 'EXP05-C') elif token.str == '(' and token.astOperand1 and token.astOperand2 and token.astOperand1.function: function = token.astOperand1.function arguments = cppcheckdata.getArguments(token.previous) if not arguments: continue for argnr, argvar in function.argument.items(): if argnr < 1 or argnr > len(arguments): continue if not argvar.isPointer: continue if (argvar.constness % 2) == 1: # data is const continue argtok = arguments[argnr - 1] if not argtok.valueType: continue if argtok.valueType.pointer == 0: continue const2 = arguments[argnr - 1].valueType.constness if (const2 % 2) == 1: reportError(token, 'style', "Attempt to cast away const", 'EXP05-C') # EXP42-C # do not compare padding data def exp42(data): for token in data.tokenlist: if token.str != '(' or not token.astOperand1 or token.astOperand1.str != 'memcmp': continue arg1 = None arg2 = None if token.astOperand2 and token.astOperand2.str == ',': if token.astOperand2.astOperand1 and token.astOperand2.astOperand1.str == ',': arg1 = token.astOperand2.astOperand1.astOperand1 arg2 = token.astOperand2.astOperand1.astOperand2 if isLocalUnpackedStruct(arg1) or isLocalUnpackedStruct(arg2): reportError( token, 'style', "Comparison of struct padding data " + "(fix either by packing the struct using '#pragma pack' or by rewriting the comparison)", 'EXP42-C') # EXP15-C # Do not place a semicolon on the same line as an if, for or while statement def exp15(data): for scope in data.scopes: if scope.type in ('If', 'For', 'While'): token = scope.bodyStart.next if token.str==';' and token.linenr==scope.bodyStart.linenr: reportError(token, 'style', 'Do not place a semicolon on the same line as an IF, FOR or WHILE', 'EXP15-C') # EXP46-C # Do not use a bitwise operator with a Boolean-like operand # int x = (a == b) & c; def exp46(data): for token in data.tokenlist: if isBitwiseOp(token) and (isComparisonOp(token.astOperand1) or isComparisonOp(token.astOperand2)): reportError( token, 'style', 'Bitwise operator is used with a Boolean-like operand', 'EXP46-c') # INT31-C # Ensure that integer conversions do not result in lost or misinterpreted data def int31(data, platform): if not platform: return for token in data.tokenlist: if not isCast(token): continue if not token.valueType or not token.astOperand1.values: continue bits = None if token.valueType.type == 'char': bits = platform.char_bit elif token.valueType.type == 'short': bits = platform.short_bit elif token.valueType.type == 'int': bits = platform.int_bit elif token.valueType.type == 'long': bits = platform.long_bit elif token.valueType.type == 'long long': bits = platform.long_long_bit else: continue if token.valueType.sign == 'unsigned': found = False for value in token.astOperand1.values: if value.intvalue and value.intvalue < 0: found = True reportError( token, 'style', 'Ensure that integer conversions do not result in lost or misinterpreted data (casting ' + str(value.intvalue) + ' to unsigned ' + token.valueType.type + ')', 'INT31-c') break if found: continue if bits >= 64: continue minval = 0 maxval = 1 if token.valueType.sign == 'signed': minval = -(1 << (bits - 1)) maxval = ((1 << (bits - 1)) - 1) else: minval = 0 maxval = ((1 << bits) - 1) for value in token.astOperand1.values: if value.intvalue and (value.intvalue < minval or value.intvalue > maxval): destType = '' if token.valueType.sign: destType = token.valueType.sign + ' ' + token.valueType.type else: destType = token.valueType.type reportError( token, 'style', 'Ensure that integer conversions do not result in lost or misinterpreted data (casting ' + str(value.intvalue) + ' to ' + destType + ')', 'INT31-c') break # ENV33-C # Do not call system() def env33(data): for token in data.tokenlist: if isFunctionCall(token, ('system',), 1): # Invalid syntax if not token.next.astOperand2: continue # ENV33-C-EX1: It is permissible to call system() with a null # pointer argument to determine the presence of a command processor # for the system. argValue = token.next.astOperand2.getValue(0) if argValue and argValue.intvalue == 0 and argValue.isKnown(): continue reportError(token, 'style', 'Do not call system()', 'ENV33-C') # MSC24-C # Do not use deprecated or obsolescent functions def msc24(data): for token in data.tokenlist: if isFunctionCall(token, ('asctime',), 1): reportError(token,'style','Do not use asctime() better use asctime_s()', 'MSC24-C') elif isFunctionCall(token, ('atof',), 1): reportError(token,'style','Do not use atof() better use strtod()', 'MSC24-C') elif isFunctionCall(token, ('atoi',), 1): reportError(token,'style','Do not use atoi() better use strtol()', 'MSC24-C') elif isFunctionCall(token, ('atol',), 1): reportError(token,'style','Do not use atol() better use strtol()', 'MSC24-C') elif isFunctionCall(token, ('atoll',), 1): reportError(token,'style','Do not use atoll() better use strtoll()', 'MSC24-C') elif isFunctionCall(token, ('ctime',), 1): reportError(token,'style','Do not use ctime() better use ctime_s()', 'MSC24-C') elif isFunctionCall(token, ('fopen',), 2): reportError(token,'style','Do not use fopen() better use fopen_s()', 'MSC24-C') elif isFunctionCall(token, ('freopen',), 3): reportError(token,'style','Do not use freopen() better use freopen_s()', 'MSC24-C') elif isFunctionCall(token, ('rewind',), 1): reportError(token,'style','Do not use rewind() better use fseek()', 'MSC24-C') elif isFunctionCall(token, ('setbuf',), 2): reportError(token,'style','Do not use setbuf() better use setvbuf()', 'MSC24-C') # MSC30-C # Do not use the rand() function for generating pseudorandom numbers def msc30(data): for token in data.tokenlist: if simpleMatch(token, "rand ( )") and isStandardFunction(token): reportError(token, 'style', 'Do not use the rand() function for generating pseudorandom numbers', 'MSC30-c') # STR03-C # Do not inadvertently truncate a string def str03(data): for token in data.tokenlist: if not isFunctionCall(token, 'strncpy'): continue arguments = cppcheckdata.getArguments(token) if len(arguments)!=3: continue if arguments[2].str=='(' and arguments[2].astOperand1.str=='sizeof': reportError(token, 'style', 'Do not inadvertently truncate a string', 'STR03-C') # STR05-C # Use pointers to const when referring to string literals def str05(data): for token in data.tokenlist: if token.isString: parent = token.astParent if parent is None: continue parentOp1 = parent.astOperand1 if parent.isAssignmentOp and parentOp1.valueType: if (parentOp1.valueType.type in ('char', 'wchar_t')) and parentOp1.valueType.pointer and not parentOp1.valueType.constness: reportError(parentOp1, 'style', 'Use pointers to const when referring to string literals', 'STR05-C') # STR07-C # Use the bounds-checking interfaces for string manipulation def str07(data): for token in data.tokenlist: if not isFunctionCall(token, ('strcpy', 'strcat')): continue args = cppcheckdata.getArguments(token) if len(args)!=2: continue if args[1].isString: continue reportError(token, 'style', 'Use the bounds-checking interfaces %s_s()' % (token.str), 'STR07-C') # STR11-C # Do not specify the bound of a character array initialized with a string literal def str11(data): for token in data.tokenlist: if not token.isString: continue strlen = token.strlen parent = token.astParent if parent is None: continue parentOp1 = parent.astOperand1 if parentOp1 is None or parentOp1.str!='[': continue if not parent.isAssignmentOp: continue varToken = parentOp1.astOperand1 if varToken is None or not varToken.isName: continue if varToken.variable is None: continue if varToken != varToken.variable.nameToken: continue valueToken = parentOp1.astOperand2 if valueToken is None: continue if valueToken.isNumber and int(valueToken.str)==strlen: reportError(valueToken, 'style', 'Do not specify the bound of a character array initialized with a string literal', 'STR11-C') # API01-C # Avoid laying out strings in memory directly before sensitive data def api01(data): for scope in data.scopes: if scope.type!='Struct': continue token = scope.bodyStart arrayFound=False # loop through the complete struct while token != scope.bodyEnd: if token.isName and token.variable: if token.variable.isArray: arrayFound=True elif arrayFound and not token.variable.isArray and not token.variable.isConst: reportError(token, 'style', 'Avoid laying out strings in memory directly before sensitive data', 'API01-C') # reset flags to report other positions in the same struct arrayFound=False token = token.next def get_args(): parser = cppcheckdata.ArgumentParser() parser.add_argument("-verify", help=argparse.SUPPRESS, action="store_true") return parser.parse_args() if __name__ == '__main__': args = get_args() if args.verify: VERIFY = True if not args.dumpfile: if not args.quiet: print("no input files.") sys.exit(0) for dumpfile in args.dumpfile: if not args.quiet: print('Checking %s...' % dumpfile) data = cppcheckdata.CppcheckData(dumpfile) if VERIFY: VERIFY_ACTUAL = [] VERIFY_EXPECTED = [] for tok in data.rawTokens: if tok.str.startswith('//') and 'TODO' not in tok.str: for word in tok.str[2:].split(' '): if re.match(r'cert-[A-Z][A-Z][A-Z][0-9][0-9].*',word): VERIFY_EXPECTED.append(str(tok.linenr) + ':' + word) for cfg in data.iterconfigurations(): if not args.quiet: print('Checking %s, config %s...' % (dumpfile, cfg.name)) exp05(cfg) exp42(cfg) exp46(cfg) exp15(cfg) int31(cfg, data.platform) str03(cfg) str05(cfg) str07(cfg) str11(cfg) env33(cfg) msc24(cfg) msc30(cfg) api01(cfg) if VERIFY: for expected in VERIFY_EXPECTED: if expected not in VERIFY_ACTUAL: print('Expected but not seen: ' + expected) sys.exit(1) for actual in VERIFY_ACTUAL: if actual not in VERIFY_EXPECTED: print('Not expected: ' + actual) sys.exit(1)