| #!/usr/bin/env python |
| # -*- coding: utf-8 -*- |
| # |
| # Copyright 2019, Data61, CSIRO (ABN 41 687 119 230) |
| # |
| # SPDX-License-Identifier: BSD-2-Clause |
| # |
| # |
| |
| from __future__ import absolute_import, division, print_function, \ |
| unicode_literals |
| from camkes.internal.tests.utils import CAmkESTest, spin_available |
| ''' |
| This file contains unit test cases related to the seL4Notification connector. |
| ''' |
| |
| |
| import os |
| import re |
| import shutil |
| import six |
| import subprocess |
| import sys |
| import unittest |
| import functools |
| from pycparser import c_ast, c_generator, c_parser |
| |
| ME = os.path.abspath(__file__) |
| MY_DIR = os.path.dirname(ME) |
| |
| # Make CAmkES importable |
| sys.path.append(os.path.join(os.path.dirname(ME), '../../..')) |
| |
| |
| class TestSel4Notification(CAmkESTest): |
| def test_locking_protocol(self): |
| ''' |
| The seL4Notification connector uses a binary semaphore internally to protect |
| accesses to callback registration. Any functions contained on the "to" |
| side, if they acquire this semaphore, are expected to release it before |
| exiting. In an early draft of the connector code, there was a single |
| execution path where we failed to release the semaphore. This test case |
| validates that we always release the semaphore when appropriate, and |
| never release it without having first acquired it. |
| |
| Note that the static analysis performed in this test case is highly |
| simplified and specialised for this connector code. It makes many |
| assumptions about code idioms and over approximations of the control |
| flow. If you are modifying the seL4Notification connector code in any |
| non-trivial way, do not expect to get by without having to modify this |
| test case. |
| |
| Some specific limitations of the static analysis: |
| - No support for `for` loops, `do-while` loops, `switch`, ternary |
| conditionals, `goto`, `asm`, attributes and more; |
| - `while` loops are treated as executing 0 or 1 times; |
| - No shortcut logic (e.g. `0 && lock()` would be considered lock |
| acquisition); |
| - No support for `break`; |
| - No restriction on recursive acquisition of the semaphore (even |
| though the one we're using doesn't support recursive acquisition); |
| - No support for // style comments; |
| - Probably more |
| ''' |
| src = munge(os.path.join(MY_DIR, '../seL4Notification-to.template.c')) |
| ast = parse(src) |
| for node in ast.ext: |
| # Test all contained functions. |
| if isinstance(node, c_ast.FuncDef): |
| check_termination(src, node.decl.name, node.body.block_items) |
| |
| @unittest.skipIf(not spin_available(), 'Spin not found') |
| def test_sel4notification_safety(self): |
| pml = os.path.join(MY_DIR, 'sel4notification.pml') |
| |
| tmp = self.mkdtemp() |
| |
| subprocess.check_call(['spin', '-a', pml], cwd=tmp) |
| |
| pan_safety = os.path.join(tmp, 'pan-safety') |
| subprocess.check_call(['gcc', '-o', pan_safety, '-O3', '-DREACH', |
| '-DSAFETY', 'pan.c'], cwd=tmp, universal_newlines=True) |
| |
| p = subprocess.Popen([pan_safety], stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, universal_newlines=True) |
| stdout, stderr = p.communicate() |
| |
| if p.returncode != 0: |
| self.fail('pan-safety returned %s' % p.returncode) |
| |
| if stdout.find('errors: 0') < 0: |
| self.fail('pan-safety failed:\n%s' % stdout) |
| |
| @unittest.skipIf(not spin_available(), 'Spin not found') |
| def test_sel4notification_liveness(self): |
| pml = os.path.join(MY_DIR, 'sel4notification.pml') |
| |
| tmp = self.mkdtemp() |
| |
| subprocess.check_call(['spin', '-a', '-m', pml], cwd=tmp) |
| |
| pan_liveness = os.path.join(tmp, 'pan-safety') |
| subprocess.check_call(['gcc', '-o', pan_liveness, '-O3', '-DNP', |
| '-DNOREDUCE', 'pan.c'], cwd=tmp) |
| |
| p = subprocess.Popen([pan_liveness, '-l', 'm100000'], |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
| universal_newlines=True) |
| stdout, stderr = p.communicate() |
| |
| if p.returncode != 0: |
| self.fail('pan-liveness returned %s' % p.returncode) |
| |
| if stdout.find('errors: 0') < 0: |
| self.fail('pan-liveness failed:\n%s' % stdout) |
| |
| |
| # A brief prelude to the seL4Notification-to.template.c source to give it some types |
| # it expects in the absence of the libsel4 headers. |
| HEADER = ''' |
| typedef int seL4_CPtr; |
| typedef unsigned long seL4_Word; |
| ''' |
| |
| |
| def munge(filename): |
| ''' |
| Tweak the seL4Notification-to.template.c source to suppress some external |
| references and constructs pycparser can't handle. |
| ''' |
| stripped = functools.reduce(lambda acc, x: re.sub(x[0], x[1], acc, flags=re.MULTILINE), |
| |
| # Turn template variable definitions into source variable definitions. |
| ((r'/\*-\s*set\s+(notification|handoff|lock)\s*=.*?-\*/', |
| r'static seL4_CPtr \g<1>;'), |
| (r'/\*-\s*set\s+badge_magic\s*=.*?-\*/', |
| r'static seL4_Word badge_magic;'), |
| |
| # Turn template variable reference into source variable references. |
| (r'/\*\?\s*(notification|handoff|lock|badge_magic)\s*\?\*/', r'\g<1>'), |
| |
| # Remove comments. |
| (r'/\*(.|\n)*?\*/', ''), |
| |
| # Remove usage of the UNUSED macro. |
| (r'\sUNUSED\s', ' '), |
| |
| # Remove CPP directives. |
| (r'^#.*?$', ''), |
| |
| # Remove CAmkES error invocations. |
| (r'ERR\((.|\n)*?{(.|\n)*?}(.|\n)*?{(.|\n)*?}\)\);', '')), |
| |
| open(filename, 'rt').read()) |
| return '%s%s' % (HEADER, stripped) |
| |
| |
| def parse(string): |
| ''' |
| Parse C source code into a pycparser AST. |
| ''' |
| return c_parser.CParser().parse(string) |
| |
| |
| def lock_operations(statement): |
| ''' |
| Find the number of `lock()` operations within a statement. Note that we |
| count `unlock()` as -1 to compute a total. This function makes many |
| simplifications, including performing no control flow analysis, and thus |
| may be inaccurate. |
| ''' |
| ops = 0 |
| if isinstance(statement, c_ast.FuncCall): |
| if statement.name.name == 'lock': |
| ops = 1 |
| elif statement.name.name == 'unlock': |
| ops = -1 |
| |
| # Descend into this node's children if it has any. |
| if isinstance(statement, c_ast.Node): |
| ops += sum(lock_operations(x) for x in statement.children()) |
| elif isinstance(statement, tuple): |
| ops += sum(lock_operations(x) for x in statement) |
| |
| return ops |
| |
| |
| class LockProtocolError(Exception): |
| ''' |
| An error indicating a failure to release a lock or too many lock releases |
| on an exit path. |
| ''' |
| |
| def __init__(self, message, source, node=None): |
| if node is not None: |
| # Compute a pretty printed version of the source code indicating the |
| # terminating line of execution that leads to the error. |
| content = six.cStringIO() |
| for lineno, line in enumerate(source.split('\n')): |
| content.write('%s %s\n' % |
| ('>' if lineno == int(node.coord.line) else ' ', line)) |
| super(LockProtocolError, self).__init__('%s\n%s' % |
| (message, content.getvalue())) |
| else: |
| super(LockProtocolError, self).__init__(message) |
| |
| |
| def check_termination(source, name, statements, accumulated=None, locks=0): |
| ''' |
| Ensure the lock protocol is preserved on all paths through this series of |
| statements. |
| |
| source - source code containing these statements (for diagnostics) |
| name - name of the containing function |
| statements - statements to analyse |
| accumulated - statements we have passed leading up to the current series |
| locks - number of locks we currently hold |
| ''' |
| |
| if accumulated is None: |
| accumulated = [] |
| |
| if statements is None: |
| statements = [] |
| |
| for index, statement in enumerate(statements): |
| |
| if isinstance(statement, c_ast.While): |
| # For `while` loops, we assume the condition is always evaluated |
| # and then the body is executed either 0 or 1 times. The recursive |
| # call here is the 1 case and the continuing (fall through) |
| # execution is the 0 case. |
| locks += lock_operations(statement.cond) |
| if locks < 0: |
| raise LockProtocolError('lock release while no lock held in ' |
| '%s at line %s' % (name, statement.coord.line), source, |
| statement) |
| check_termination(source, name, statement.stmt.block_items + |
| statements[index+1:], accumulated + statements[:index+1], |
| locks) |
| |
| elif isinstance(statement, c_ast.If): |
| # For `if` statements, we assume any lock operations in the |
| # condition expression are only relevant for the else branch. This |
| # is based on the following idiom for taking a lock: |
| # |
| # if (lock() != 0) { |
| # /* failure case; lock() should not be counted */ |
| # } else { |
| # /* success case; lock() should be counted */ |
| # } |
| |
| # If branch. |
| check_termination(source, name, ([statement.iftrue] if |
| statement.iftrue is not None else []) + statements[index+1:], |
| accumulated + statements[:index+1], locks) |
| |
| # Else branch. |
| locks += lock_operations(statement.cond) |
| if locks < 0: |
| raise LockProtocolError('lock release while no lock held in ' |
| '%s at else branch of line %s' % (name, |
| statement.coord.line), source, statement) |
| check_termination(source, name, ([statement.iffalse] if |
| statement.iffalse is not None else []) + statements[index+1:], |
| accumulated + statements[:index+1], locks) |
| |
| # Together, the recursive calls have handled the remainder of this |
| # function, so no need to continue. |
| return |
| |
| elif isinstance(statement, (c_ast.Return, c_ast.Continue)): |
| # We've reached a termination point of this function. Note that we |
| # count a `continue` statement as a point of termination. This is |
| # based on the following idiom for restarting a function, which we |
| # assume is the only use of `continue`: |
| # |
| # void foo() { |
| # while (true) { |
| # ... |
| # continue; /* restart function */ |
| # ... |
| # } |
| # } |
| |
| # Take into account any lock operations in the return expression |
| # itself, though we expect there to be none. |
| locks += lock_operations(statement) |
| |
| if locks > 0: |
| raise LockProtocolError('%s locks potentially held when ' |
| 'exiting %s at line %s' % (locks, name, |
| statement.coord.line), source, statement) |
| elif locks < 0: |
| raise LockProtocolError('%s too many lock releases ' |
| 'potentially executed before exiting %s at line %s' % |
| (-locks, name, statement.coord.line), source, statement) |
| |
| # Otherwise, we're good. |
| |
| return |
| |
| elif isinstance(statement, c_ast.Compound): |
| # Statement block. Recurse into it. |
| check_termination(source, name, (statement.block_items or []) + |
| statements[index+1:], accumulated + statements[:index+1], |
| locks) |
| return |
| |
| else: |
| # Regular statement. Count its locks. |
| locks += lock_operations(statement) |
| |
| # If we reached here, this function terminated without a return statement |
| # (perfectly valid for a `void` function). |
| |
| # Find the last statement executed in case we need to raise an exception. |
| if len(statements) > 0: |
| last = statements[-1] |
| elif len(accumulated) > 0: |
| last = accumulated[-1] |
| else: |
| last = None |
| |
| if last is None: |
| location = 'end of function' |
| else: |
| location = 'line %s' % last.coord.line |
| |
| if locks > 0: |
| raise LockProtocolError('%s locks potentially held when exiting %s at ' |
| '%s' % (locks, name, location), source, last) |
| elif locks < 0: |
| raise LockProtocolError('%s too many lock releases potentially ' |
| 'executed before exiting %s at %s' % (-locks, name, location), |
| source, last) |
| |
| # If we reached here, the function conformed to the protocol in an |
| # execution that reached its end. |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |