diff mbox series

[RFC,v2,07/10] static-analyzer: Enforce coroutine_fn restrictions on function pointers

Message ID 20220729130040.1428779-8-afaria@redhat.com
State New
Headers show
Series Introduce an extensible static analyzer | expand

Commit Message

Alberto Faria July 29, 2022, 1 p.m. UTC
Extend static-analyzer.py's "coroutine_fn" check to enforce coroutine_fn
restrictions on function pointer operations.

Invalid operations include assigning a coroutine_fn value to a
non-coroutine_fn function pointer, and invoking a coroutine_fn function
pointer from a non-coroutine_fn function.

Signed-off-by: Alberto Faria <afaria@redhat.com>
---
 static_analyzer/__init__.py     |  27 ++++++++
 static_analyzer/coroutine_fn.py | 115 ++++++++++++++++++++++++++++++--
 2 files changed, 138 insertions(+), 4 deletions(-)
diff mbox series

Patch

diff --git a/static_analyzer/__init__.py b/static_analyzer/__init__.py
index 5abdbd21a3..90992d3500 100644
--- a/static_analyzer/__init__.py
+++ b/static_analyzer/__init__.py
@@ -24,6 +24,8 @@ 
     Cursor,
     CursorKind,
     SourceLocation,
+    SourceRange,
+    TokenGroup,
     TranslationUnit,
     TypeKind,
     conf,
@@ -117,6 +119,31 @@  def actual_visitor(node: Cursor, parent: Cursor, client_data: None) -> int:
 # Node predicates
 
 
+def is_binary_operator(node: Cursor, operator: str) -> bool:
+    return (
+        node.kind == CursorKind.BINARY_OPERATOR
+        and get_binary_operator_spelling(node) == operator
+    )
+
+
+def get_binary_operator_spelling(node: Cursor) -> Optional[str]:
+
+    assert node.kind == CursorKind.BINARY_OPERATOR
+
+    [left, right] = node.get_children()
+
+    op_range = SourceRange.from_locations(left.extent.end, right.extent.start)
+
+    tokens = list(TokenGroup.get_tokens(node.translation_unit, op_range))
+    if not tokens:
+        # Can occur when left and right children extents overlap due to
+        # misparsing.
+        return None
+
+    [op_token, *_] = tokens
+    return op_token.spelling
+
+
 def might_have_attribute(node: Cursor, attr: Union[CursorKind, str]) -> bool:
     """
     Check whether any of `node`'s children are an attribute of the given kind,
diff --git a/static_analyzer/coroutine_fn.py b/static_analyzer/coroutine_fn.py
index f70a3167eb..a16dcbeb52 100644
--- a/static_analyzer/coroutine_fn.py
+++ b/static_analyzer/coroutine_fn.py
@@ -8,6 +8,7 @@ 
     check,
     is_annotated_with,
     is_annotation,
+    is_binary_operator,
     is_comma_wrapper,
     visit,
 )
@@ -22,6 +23,7 @@  def check_coroutine_fn(context: CheckContext) -> None:
     def visitor(node: Cursor) -> VisitorResult:
 
         validate_annotations(context, node)
+        check_function_pointers(context, node)
 
         if node.kind == CursorKind.FUNCTION_DECL and node.is_definition():
             check_direct_calls(context, node)
@@ -91,6 +93,83 @@  def visitor(node: Cursor) -> VisitorResult:
         visit(caller, visitor)
 
 
+def check_function_pointers(context: CheckContext, node: Cursor) -> None:
+
+    # What we would really like is to associate annotation attributes with types
+    # directly, but that doesn't seem possible. Instead, we have to look at the
+    # relevant variable/field/parameter declarations, and follow typedefs.
+
+    # This doesn't check all possible ways of assigning to a coroutine_fn
+    # field/variable/parameter. That would probably be too much work.
+
+    # TODO: Check struct/union/array initialization.
+    # TODO: Check assignment to struct/union/array fields.
+
+    # check initialization of variables using coroutine_fn values
+
+    if node.kind == CursorKind.VAR_DECL:
+
+        children = [
+            c
+            for c in node.get_children()
+            if c.kind
+            not in [
+                CursorKind.ANNOTATE_ATTR,
+                CursorKind.INIT_LIST_EXPR,
+                CursorKind.TYPE_REF,
+                CursorKind.UNEXPOSED_ATTR,
+            ]
+        ]
+
+        if (
+            len(children) == 1
+            and not is_coroutine_fn(node)
+            and is_coroutine_fn(children[0])
+        ):
+            context.report(node, "assigning coroutine_fn to non-coroutine_fn")
+
+    # check initialization of fields using coroutine_fn values
+
+    # TODO: This only checks designator initializers.
+
+    if node.kind == CursorKind.INIT_LIST_EXPR:
+
+        for initializer in filter(
+            lambda n: n.kind == CursorKind.UNEXPOSED_EXPR,
+            node.get_children(),
+        ):
+
+            children = list(initializer.get_children())
+
+            if (
+                len(children) == 2
+                and children[0].kind == CursorKind.MEMBER_REF
+                and not is_coroutine_fn(children[0].referenced)
+                and is_coroutine_fn(children[1])
+            ):
+                context.report(
+                    initializer,
+                    "assigning coroutine_fn to non-coroutine_fn",
+                )
+
+    # check assignments of coroutine_fn values to variables or fields
+
+    if is_binary_operator(node, "="):
+
+        [left, right] = node.get_children()
+
+        if (
+            left.kind
+            in [
+                CursorKind.DECL_REF_EXPR,
+                CursorKind.MEMBER_REF_EXPR,
+            ]
+            and not is_coroutine_fn(left.referenced)
+            and is_coroutine_fn(right)
+        ):
+            context.report(node, "assigning coroutine_fn to non-coroutine_fn")
+
+
 # ---------------------------------------------------------------------------- #
 
 
@@ -138,7 +217,13 @@  def is_valid_allow_coroutine_fn_call_usage(node: Cursor) -> bool:
     `node` appears at a valid point in the AST. This is the case if its right
     operand is a call to:
 
-      - A function declared with the `coroutine_fn` annotation.
+      - A function declared with the `coroutine_fn` annotation, OR
+      - A field/variable/parameter whose declaration has the `coroutine_fn`
+        annotation, and of function pointer type, OR
+      - [TODO] A field/variable/parameter of a typedef function pointer type,
+        and the typedef has the `coroutine_fn` annotation, OR
+      - [TODO] A field/variable/parameter of a pointer to typedef function type,
+        and the typedef has the `coroutine_fn` annotation.
 
     TODO: Ensure that `__allow_coroutine_fn_call()` is in the body of a
     non-`coroutine_fn` function.
@@ -165,9 +250,31 @@  def is_coroutine_fn(node: Cursor) -> bool:
         else:
             break
 
-    return node.kind == CursorKind.FUNCTION_DECL and is_annotated_with(
-        node, "coroutine_fn"
-    )
+    if node.kind in [CursorKind.DECL_REF_EXPR, CursorKind.MEMBER_REF_EXPR]:
+        node = node.referenced
+
+    # ---
+
+    if node.kind == CursorKind.FUNCTION_DECL:
+        return is_annotated_with(node, "coroutine_fn")
+
+    if node.kind in [
+        CursorKind.FIELD_DECL,
+        CursorKind.VAR_DECL,
+        CursorKind.PARM_DECL,
+    ]:
+
+        if is_annotated_with(node, "coroutine_fn"):
+            return True
+
+        # TODO: If type is typedef or pointer to typedef, follow typedef.
+
+        return False
+
+    if node.kind == CursorKind.TYPEDEF_DECL:
+        return is_annotated_with(node, "coroutine_fn")
+
+    return False
 
 
 # ---------------------------------------------------------------------------- #