File size: 48,916 Bytes
ab54eb4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 | """
Tree-sitter C parser for call graph extraction.
Tries to use tree-sitter + tree-sitter-c; falls back to regex-based
extraction if the grammar is not available.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------
@dataclass
class FunctionSignature:
"""Parsed signature of a C function."""
name: str
return_type: str
parameters: list[tuple[str, str]] # [(type, name), ...]
is_static: bool = False # True if declared with `static` storage class
@dataclass
class FunctionInfo:
"""All information about a single C function."""
name: str
signature: FunctionSignature
body: str # source text of function body
callees: set[str] # functions this function calls
source_file: str
@dataclass
class ParsedCFile:
"""Result of parsing a C source file."""
path: str
functions: dict[str, FunctionSignature] # name -> signature
call_graph: dict[str, set[str]] # caller -> set of callee names
function_bodies: dict[str, str] # name -> raw body text
# Full function-definition text (return type + attributes + declarator + body)
# captured directly from tree-sitter. Used by the harness generator to
# excise complete function defs from the source when emitting type-decl
# context, so multi-line return types and attribute lines don't leak through
# as orphan declarations.
function_definitions: dict[str, str] = field(default_factory=dict)
# Struct definitions encountered at translation-unit scope. Keyed by
# the struct's tag name (the part after ``struct``, or the typedef'd
# alias when bound via ``typedef struct { ... } Name;``). The value is
# a list of ``(field_type, field_name)`` pairs preserving declaration
# order. Used by the harness emitter to populate struct-pointer params
# with per-field initialisation (pointer fields β fresh backing
# buffers; length/index fields β ``>= 0`` constraint) so opaque-struct
# arguments don't produce 100+ spurious CBMC field-access findings.
struct_definitions: dict[str, list[tuple[str, str]]] = field(default_factory=dict)
# When the file was preprocessed before parsing, the expanded source is
# stored here so harness generators can use it instead of re-reading the
# original (unexpanded) file.
preprocessed_source: Optional[str] = None
# cpp ``# N "filename"`` line directives let us tell which header (or
# the original .c) each function body came from. Populated only when the
# parser sees those directives in the input (i.e. a preprocessed ``.i``
# or ``.c`` file dumped from ``make foo.i``). Empty otherwise.
# Keyed by function name (matches ``functions``); value is the originating
# source path as written in the cpp directive (e.g.
# ``drivers/usb/serial/ch341.c`` or ``./include/linux/usb.h``).
function_source_files: dict[str, str] = field(default_factory=dict)
# Primary source the TU came from β taken from the first ``# N "..."``
# directive at line 1. For a preprocessed kernel driver, this is the
# original .c file. None for non-preprocessed input. Used by
# ``restrict_to_primary_source`` to drop header-inlined functions
# (kernel preprocessing inlines several thousand ``static inline``
# helpers from ``linux/*.h``; without filtering, the pipeline tries
# to spec all of them).
primary_source: Optional[str] = None
# Functions whose ADDRESS is taken inside another function's body
# (passed as a callback argument, stored in a struct field, etc.).
# Used by Phase 3's caller-feasibility check to handle vtable-
# dispatched functions: when the FUT has no direct callers (because
# it's only invoked via a function pointer), the address-takers
# serve as evidence of indirect reachability. Without this, Phase 3
# marks every libarchive format-reader / vtable-callback function
# as "unresolved", suppressing real bugs in those code paths.
#
# Keyed by function name; value is a set of caller-function names
# whose body contains the function's identifier in a non-call
# position (no immediately-following ``(``). Conservative β picks
# up some non-callback references too, but those are harmless for
# Phase 3's "is the function reachable from any in-tree usage"
# question.
address_taken_in: dict[str, set[str]] = field(default_factory=dict)
def restrict_to_primary_source(self) -> int:
"""Drop functions whose body did NOT originate in
``self.primary_source``. No-op if ``primary_source`` is None or
``function_source_files`` is empty (i.e. the input wasn't
preprocessed and we have no provenance info).
Returns the number of functions dropped, so the caller can log
the filtering action.
"""
if not self.primary_source or not self.function_source_files:
return 0
primary_base = self.primary_source.rsplit("/", 1)[-1]
keep: set[str] = set()
for name, origin in self.function_source_files.items():
if not origin:
continue
if origin == self.primary_source:
keep.add(name)
continue
# Match on basename too β cpp may show the same file with
# different prefixes (``./drivers/...`` vs ``drivers/...``)
# depending on how the build was invoked.
if origin.rsplit("/", 1)[-1] == primary_base:
keep.add(name)
dropped = [n for n in list(self.functions) if n not in keep]
for n in dropped:
self.functions.pop(n, None)
self.function_bodies.pop(n, None)
self.function_definitions.pop(n, None)
self.call_graph.pop(n, None)
self.function_source_files.pop(n, None)
# Don't prune call-graph edges. Each primary-file function's
# callee set was populated from its own body and lists every
# call site verbatim, including kernel-header inlines like
# ``phy_write``. Dropping those edges leaves the harness
# generator unable to recognise the callee and emit a proper
# stub, which is fatal when the dropped-but-called inline
# exercises CBMC-unsupported features (anonymous-tag struct
# inclusion, statement-expression macros). Keeping the edge
# treats header inlines uniformly with truly external symbols.
return len(dropped)
def get_function_info(self, name: str) -> Optional["FunctionInfo"]:
"""Return a FunctionInfo for the named function, or None if not found."""
if name not in self.functions:
return None
return FunctionInfo(
name=name,
signature=self.functions[name],
body=self.function_bodies.get(name, ""),
callees=self.call_graph.get(name, set()),
source_file=self.path,
)
def all_function_infos(self) -> list["FunctionInfo"]:
"""Return FunctionInfo for every parsed function."""
return [self.get_function_info(n) for n in self.functions] # type: ignore[misc]
# ---------------------------------------------------------------------------
# Tree-sitter setup (optional)
# ---------------------------------------------------------------------------
_TS_AVAILABLE = False
_TS_LANGUAGE = None
def _try_load_tree_sitter() -> None:
"""Attempt to load the tree-sitter C grammar; set _TS_AVAILABLE on success."""
global _TS_AVAILABLE, _TS_LANGUAGE
if _TS_AVAILABLE:
return
try:
import tree_sitter_c as tsc
from tree_sitter import Language
# tree-sitter >= 0.22 exposes language() as a capsule
if hasattr(tsc, "language"):
_TS_LANGUAGE = Language(tsc.language())
else:
# Older binding: Language(path, name)
_TS_LANGUAGE = Language(tsc.__file__, "c")
_TS_AVAILABLE = True
except Exception:
_TS_AVAILABLE = False
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def parse_c_file(
path: str | Path,
source_text: Optional[str] = None,
) -> ParsedCFile:
"""
Parse a C source file and return function signatures + call graph.
Parameters
----------
path:
Path to the original ``.c`` file (used for artifact naming).
source_text:
If provided, parse this string instead of reading *path* from disk.
Use this to pass preprocessed / expanded source.
Uses tree-sitter if available; otherwise falls back to regex.
"""
path = Path(path)
provided_source = source_text is not None
if source_text is None:
source_bytes = path.read_bytes()
source_text = source_bytes.decode("utf-8", errors="replace")
else:
source_bytes = source_text.encode("utf-8", errors="replace")
_try_load_tree_sitter()
if _TS_AVAILABLE:
try:
result = _parse_with_tree_sitter(source_bytes, source_text, str(path))
if provided_source or result.primary_source:
# ``primary_source`` is populated from cpp ``# N "..."``
# line directives, which only appear when the input was
# preprocessed. Treating "directives present" as a
# synonym for "preprocessed" lets the harness emitter
# skip its libc-header prepend (which conflicts with the
# inlined glibc/kernel types).
result.preprocessed_source = source_text
return result
except Exception:
pass # fall through to regex
result = _parse_with_regex(source_text, str(path))
if provided_source or result.primary_source:
result.preprocessed_source = source_text
return result
# ---------------------------------------------------------------------------
# Tree-sitter implementation
# ---------------------------------------------------------------------------
def _build_line_to_source_map(source: str) -> tuple[list[str], Optional[str]]:
"""Walk cpp ``# N "filename" [flags]`` line directives and return
``(line_to_source, primary_source)``:
* ``line_to_source[i]`` is the originating source filename for the
0-indexed line ``i`` (empty string for lines that fall outside any
directive).
* ``primary_source`` is the first non-cpp-synthetic filename seen
(i.e. the original ``.c`` the TU was built from), or ``None`` if
the input has no cpp line directives.
The map covers each non-directive line. Directive lines themselves
get the file they introduce β they're tagged with the same source as
the lines below them.
"""
lines = source.split("\n")
line_to_source: list[str] = [""] * len(lines)
primary: Optional[str] = None
current: str = ""
# Match ``# 12 "file.c"`` (with optional trailing flag digits)
pat = re.compile(r'^#\s+\d+\s+"([^"]+)"')
for i, line in enumerate(lines):
m = pat.match(line)
if m:
current = m.group(1)
# First "real" source seen β synthetic ones look like
# ``<built-in>``, ``<command-line>``, ``<stdin>``.
if primary is None and not (current.startswith("<") and current.endswith(">")):
primary = current
line_to_source[i] = current
return line_to_source, primary
def _parse_with_tree_sitter(src_bytes: bytes, source: str, path: str) -> ParsedCFile:
"""Parse using tree-sitter. Uses byte offsets for all node slicing."""
from tree_sitter import Parser
parser = Parser(_TS_LANGUAGE)
tree = parser.parse(src_bytes)
root = tree.root_node
functions: dict[str, FunctionSignature] = {}
call_graph: dict[str, set[str]] = {}
function_bodies: dict[str, str] = {}
function_definitions: dict[str, str] = {}
function_source_files: dict[str, str] = {}
line_to_source, primary_source = _build_line_to_source_map(source)
# Preprocessor wrapper node types whose children must also be walked.
# Without recursing into these, functions guarded by ``#ifndef
# CURL_DISABLE_PARSEDATE`` (curl/parsedate.c) or ``#ifdef __linux__``
# are invisible to the parser even though they're in the build by
# default.
#
# ``compound_statement`` and ``ERROR`` appear at the top level only
# under tree-sitter's parse-error recovery: when a macro-heavy kernel
# body (FIELD_PREP nests, _Static_assert inside struct{} type-exprs)
# confuses the C grammar, tree-sitter wraps a span of trailing
# function_definitions into a synthetic ``compound_statement`` (or
# ``ERROR``) child of the translation_unit instead of failing the
# whole parse. The functions inside are still valid; we just need to
# walk through the wrapper. Without this, ~15% of the functions in
# ``drivers/net/ethernet/airoha/airoha_eth.i`` go invisible and the
# harness emitter's body-excision misses their definitions, leaving
# 73KB of orphaned ``FIELD_PREP`` expansions in the type-decls.
_PREPROC_CONTAINER_TYPES = {
"preproc_if", "preproc_ifdef", "preproc_ifndef",
"preproc_else", "preproc_elif", "preproc_elifdef", "preproc_elifndef",
"linkage_specification", # extern "C" { ... }
"compound_statement", # parse-error recovery wrapper (kernel TUs)
"ERROR", # parse-error recovery wrapper (kernel TUs)
}
def _collect_function_defs(node):
"""Yield every function_definition node, recursing through
preprocessor / linkage wrappers and parse-error-recovery
compound_statement wrappers.
At a ``function_definition`` we yield the node *and* recurse
into its compound_statement body. The recursion is needed
because tree-sitter's error recovery on macro-heavy kernel TUs
often nests trailing functions inside an earlier function's
body (parent chain: ``function_definition β compound_statement
β function_definition``). Without this, ``hid-pidff.c``'s
``pidff_rescale`` and ~10 siblings vanish. Real GCC nested
function defs are extremely rare in kernel/driver code, so the
spurious recurse cost is negligible.
"""
if node.type == "function_definition":
yield node
for child in node.children:
if child.type == "compound_statement":
for sub in child.children:
yield from _collect_function_defs(sub)
return
if node.type in _PREPROC_CONTAINER_TYPES or node.type == "translation_unit":
for child in node.children:
yield from _collect_function_defs(child)
for node in _collect_function_defs(root):
sig = _extract_sig_ts(node, src_bytes)
if sig:
# Tree-sitter's parse-error tolerance occasionally reports a
# function_definition end_byte that lands on a `}` belonging
# to an inner GCC statement-expression ``({ ... })`` rather
# than the actual function close. This leaves orphan body
# statements after end_byte that the harness emitter's body
# excision misses, leading to "syntax error before 'if'"
# in CBMC. Detect this by brace-counting the captured text;
# if the count is positive (more ``{`` than ``}``), walk
# forward from end_byte until balanced.
true_end = _brace_balanced_end_byte(
src_bytes, node.start_byte, node.end_byte
)
new_def_text = src_bytes[
node.start_byte:true_end
].decode("utf-8", errors="replace")
# Body is the compound_statement child
body_node = node.child_by_field_name("body")
new_body_text = ""
new_callees: set[str] = set()
if body_node:
body_end = _brace_balanced_end_byte(
src_bytes, body_node.start_byte, body_node.end_byte
)
new_body_text = src_bytes[
body_node.start_byte:body_end
].decode("utf-8", errors="replace")
_collect_calls_ts(body_node, new_callees, src_bytes)
# Two-definition disambiguation: when a function name appears
# twice in the TU (typical ``#ifdef CURL_DISABLE_X ... #else
# static stub ... #endif`` shape β tree-sitter parses both
# branches because it doesn't process preprocessor), prefer
# the entry with the LONGER body. Stubs are short
# (``{ (void)x; return 0; }``); real implementations are
# multi-statement. Without this, the empty-body stub
# overwrites the real one, the call graph for the real
# function vanishes, and any function the real one called
# gets mis-classified as a system-entry point.
# Regression: curl/parsedate.c run 2026-05-19 flagged
# ``datenum`` and ``time2epoch`` as caller-less entry points
# (false confirmed_system_entry verdicts) because the
# ``#else`` stub of ``parsedate`` overwrote the real body.
prev_body = function_bodies.get(sig.name)
if prev_body is not None and len(new_body_text) <= len(prev_body):
continue
functions[sig.name] = sig
call_graph[sig.name] = new_callees
function_definitions[sig.name] = new_def_text
row = node.start_point[0]
if 0 <= row < len(line_to_source):
function_source_files[sig.name] = line_to_source[row]
if new_body_text:
function_bodies[sig.name] = new_body_text
struct_definitions = _collect_struct_defs(root, src_bytes)
address_taken_in = _compute_address_takers(functions, function_bodies)
return ParsedCFile(
path=path,
functions=functions,
call_graph=call_graph,
function_bodies=function_bodies,
function_definitions=function_definitions,
struct_definitions=struct_definitions,
function_source_files=function_source_files,
primary_source=primary_source,
address_taken_in=address_taken_in,
)
def _brace_balanced_end_byte(src_bytes: bytes, start: int, ts_end: int) -> int:
"""Return the byte offset of the function's true closing ``}``.
Tree-sitter occasionally truncates a function_definition's
``end_byte`` on macro-heavy kernel bodies (FIELD_PREP +
_Static_assert inside ``struct{}`` inside GCC statement-expressions
``({ ... })``). The grammar mistakes a ``}`` of an inner expression
for the body close.
Count ``{`` / ``}`` over the captured slice, skipping over string
literals, character literals, and ``/* */`` / ``//`` comments. If
the count is positive (more ``{`` than ``}``), walk forward from
``ts_end`` byte-by-byte (using the same skip rules) until the count
reaches zero. Return that offset (inclusive of the final ``}``).
Conservative fallback: if walking forward never balances within a
safety cap, return the original ``ts_end`` unchanged. Better to
leave a faulty bound than chew the rest of the TU.
"""
# Phase 1: scan captured slice and compute imbalance + end-position
# of the scanner inside the slice. We then continue the scanner
# past ``ts_end`` if needed.
depth = 0
i = start
end = ts_end
n = len(src_bytes)
# Safety cap: don't walk more than 200KB beyond ts_end. The largest
# kernel function we've encountered is ~225KB; 200KB beyond is
# enough headroom for the recovery while preventing runaway scans
# on truly broken input.
cap = min(n, ts_end + 200_000)
def _skip_string_or_char(j: int, quote: int) -> int:
j += 1
while j < n and src_bytes[j] != quote:
if src_bytes[j] == 0x5C: # backslash
j += 2
else:
j += 1
return j + 1
def _skip_block_comment(j: int) -> int:
k = src_bytes.find(b"*/", j + 2)
return k + 2 if k != -1 else n
def _skip_line_comment(j: int) -> int:
k = src_bytes.find(b"\n", j + 2)
return k + 1 if k != -1 else n
# Walk through [start, end) first to compute depth at ts_end.
while i < end:
b = src_bytes[i]
if b == 0x22: # "
i = _skip_string_or_char(i, 0x22)
continue
if b == 0x27: # '
i = _skip_string_or_char(i, 0x27)
continue
if b == 0x2F and i + 1 < n:
nxt = src_bytes[i + 1]
if nxt == 0x2A:
i = _skip_block_comment(i)
continue
if nxt == 0x2F:
i = _skip_line_comment(i)
continue
if b == 0x7B: # {
depth += 1
elif b == 0x7D: # }
depth -= 1
if depth == 0:
# tree-sitter's end is correct; nothing to do.
return end
i += 1
# Tree-sitter's end_byte was reached but the captured slice has
# depth != 0. If depth < 0, we already over-shot inside the slice
# β leave as-is (rare; means tree-sitter included a stray ``}``).
if depth <= 0:
return end
# Phase 2: continue scanning past ts_end until balanced.
i = end
while i < cap:
b = src_bytes[i]
if b == 0x22:
i = _skip_string_or_char(i, 0x22)
continue
if b == 0x27:
i = _skip_string_or_char(i, 0x27)
continue
if b == 0x2F and i + 1 < n:
nxt = src_bytes[i + 1]
if nxt == 0x2A:
i = _skip_block_comment(i)
continue
if nxt == 0x2F:
i = _skip_line_comment(i)
continue
if b == 0x7B:
depth += 1
elif b == 0x7D:
depth -= 1
if depth == 0:
return i + 1 # inclusive of closing ``}``
i += 1
# Couldn't balance within cap β give up and keep ts_end.
return ts_end
def _collect_struct_defs(root, src_bytes: bytes) -> dict[str, list[tuple[str, str]]]:
"""Walk the translation unit and collect struct definitions, keyed by
tag name (or typedef'd alias for anonymous structs).
Each value is a list of ``(field_type, field_name)`` pairs in
declaration order. Forward declarations (``struct opaque;``) are
skipped because they have no field_declaration_list.
"""
_PREPROC_CONTAINER_TYPES = {
"preproc_if", "preproc_ifdef", "preproc_ifndef",
"preproc_else", "preproc_elif", "preproc_elifdef", "preproc_elifndef",
"linkage_specification",
# Tree-sitter parse-recovery wrappers. On large preprocessed kernel
# TUs (e.g. rtltool.i / r8125_rss.i) a recovery error earlier in
# the file causes tree-sitter to put subsequent top-level
# declarations β including ``struct rtl8125_private`` β inside a
# phantom ``function_definition > compound_statement`` block.
# Recurse into both so the collector still finds the struct
# (rtl8125 OOT batch, 2026-05-18). The same fix was applied to
# _collect_function_defs in 2026-05-18 for buried nested
# function bodies.
"function_definition", "compound_statement",
"ERROR",
}
structs: dict[str, list[tuple[str, str]]] = {}
# Aliases declared via a separate ``typedef struct Tag Alias;``
# statement, where the struct body lives in another translation-unit
# node. We can't resolve these until the full walk completes.
pending_aliases: list[tuple[str, str]] = []
def walk(node):
if node.type == "struct_specifier":
_record_struct(node, src_bytes, structs, alias=None)
return
if node.type == "type_definition":
# Two cases:
# (1) ``typedef struct [Tag] { ... } Alias;`` β body present, record under tag+alias.
# (2) ``typedef struct Tag Alias;`` β separate-typedef form,
# body lives in a sibling struct_specifier elsewhere. We
# record the aliasβtag mapping here and rebind it after
# the walk completes (alias may point to a struct whose
# body hasn't been visited yet).
inner_struct = None
inner_struct_has_body = False
inner_struct_tag = None
alias = None
for c in node.children:
if c.type == "struct_specifier":
inner_struct = c
for cc in c.children:
if cc.type == "type_identifier":
inner_struct_tag = src_bytes[cc.start_byte:cc.end_byte].decode(
"utf-8", errors="replace"
)
elif cc.type == "field_declaration_list":
inner_struct_has_body = True
elif c.type == "type_identifier":
alias = src_bytes[c.start_byte:c.end_byte].decode(
"utf-8", errors="replace"
)
if inner_struct is not None and inner_struct_has_body:
_record_struct(inner_struct, src_bytes, structs, alias=alias)
elif alias and inner_struct_tag:
# Pending alias β resolve after the full walk so we pick up
# the struct body that appears later in the file.
pending_aliases.append((alias, inner_struct_tag))
return
if node.type in _PREPROC_CONTAINER_TYPES or node.type == "translation_unit":
for c in node.children:
walk(c)
walk(root)
# Resolve pending typedef aliases: ``typedef struct Tag Alias;`` is
# common in libxml2 / libcurl / OpenSSL headers, where the struct
# body and the typedef are separate statements. Without this, harness
# generation falls back to a flat ``Type x;`` nondet rather than the
# per-field init path, and self-referential pointer fields stay
# symbolic, producing linked-list traversal false positives.
for alias, tag in pending_aliases:
if tag in structs and alias not in structs:
structs[alias] = structs[tag]
# libxml2 / glib idiom: struct tag with leading underscore, typedef
# alias without (typedef and struct usually live in a public header
# we don't parse). Rather than fight headers in real-libc mode, infer
# the alias from the convention: ``struct _xmlPattern`` β ``xmlPattern``.
for tag in list(structs.keys()):
if tag.startswith("_"):
alias = tag[1:]
if alias and alias not in structs:
structs[alias] = structs[tag]
return structs
def _record_struct(
struct_node,
src_bytes: bytes,
structs: dict[str, list[tuple[str, str]]],
alias: Optional[str],
) -> None:
"""Pull (type, name) field pairs out of a struct_specifier and store
them under the tag name and/or typedef alias."""
tag_name: Optional[str] = None
fdecl_list = None
for c in struct_node.children:
if c.type == "type_identifier":
tag_name = src_bytes[c.start_byte:c.end_byte].decode(
"utf-8", errors="replace"
)
elif c.type == "field_declaration_list":
fdecl_list = c
if fdecl_list is None:
# Forward declaration / opaque struct β no fields to extract.
return
fields: list[tuple[str, str]] = []
for fdecl in fdecl_list.children:
if fdecl.type != "field_declaration":
continue
# Gather the field type prefix (everything before the declarator).
type_parts: list[str] = []
declarator_node = None
for c in fdecl.children:
if c.type in {
"type_qualifier", "primitive_type", "type_identifier",
"sized_type_specifier", "struct_specifier",
"union_specifier", "enum_specifier",
}:
# For nested struct/union specifiers, prefer the type
# identifier ("struct Curl_str") rather than the full body.
if c.type == "struct_specifier":
tag = None
for cc in c.children:
if cc.type == "type_identifier":
tag = src_bytes[cc.start_byte:cc.end_byte].decode(
"utf-8", errors="replace"
)
type_parts.append(f"struct {tag}" if tag else "struct")
else:
type_parts.append(
src_bytes[c.start_byte:c.end_byte].decode(
"utf-8", errors="replace"
)
)
elif c.type in {
"field_identifier", "pointer_declarator", "array_declarator",
}:
declarator_node = c
if declarator_node is None:
continue
# Walk the declarator to recover the field name and any pointer
# stars / array brackets that belong to the type prefix.
name, type_suffix = _flatten_declarator(declarator_node, src_bytes)
if not name:
continue
ftype = " ".join(type_parts) + type_suffix
fields.append((ftype.strip(), name))
if not fields:
return
if tag_name:
structs[tag_name] = fields
if alias and alias not in structs:
structs[alias] = fields
def _flatten_declarator(decl_node, src_bytes: bytes) -> tuple[str, str]:
"""Return (field_name, type_suffix) for a field declarator.
``type_suffix`` accumulates ``*`` stars (pointer_declarator) and
``[N]`` array dimensions (array_declarator) so callers can append
them to the type prefix.
"""
suffix = ""
node = decl_node
while True:
if node.type == "pointer_declarator":
suffix = "*" + suffix
inner = node.child_by_field_name("declarator")
if inner is None:
return ("", suffix)
node = inner
elif node.type == "array_declarator":
# Capture the [N] portion verbatim.
text = src_bytes[node.start_byte:node.end_byte].decode(
"utf-8", errors="replace"
)
# The trailing bracket-segment is everything after the inner
# declarator's name; recover the name by recursing into the
# inner child.
inner = node.child_by_field_name("declarator")
if inner is None:
return ("", suffix)
# The bracket portion of this array declarator goes to suffix.
bracket_idx = text.find("[")
if bracket_idx >= 0:
suffix = suffix + text[bracket_idx:]
node = inner
elif node.type == "field_identifier":
name = src_bytes[node.start_byte:node.end_byte].decode(
"utf-8", errors="replace"
)
return (name, suffix)
else:
return ("", suffix)
def _slice_bytes(src_bytes: bytes, node) -> str:
"""Decode a node's byte range from src_bytes."""
return src_bytes[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
def _extract_sig_ts(node, src_bytes: bytes) -> Optional[FunctionSignature]:
"""Extract FunctionSignature from a tree-sitter function_definition node."""
declarator = node.child_by_field_name("declarator")
if declarator is None:
return None
# Unwrap pointer_declarator(s), counting stars so void *malloc -> "void *"
pointer_stars = ""
while declarator.type == "pointer_declarator":
pointer_stars += "*"
declarator = declarator.child_by_field_name("declarator") or declarator
fn_name = ""
params: list[tuple[str, str]] = []
if declarator.type == "function_declarator":
name_node = declarator.child_by_field_name("declarator")
if name_node:
fn_name = _slice_bytes(src_bytes, name_node).strip()
param_list = declarator.child_by_field_name("parameters")
if param_list:
siblings = list(param_list.named_children)
for idx, child in enumerate(siblings):
if child.type == "parameter_declaration":
p_type, p_name = _extract_param_ts(child, src_bytes)
# Recover from tree-sitter's misparse of
# ``T * MACRO name`` where MACRO is an unknown
# qualifier (GGML_RESTRICT, __restrict__,
# __attribute__((nonnull)), GGML_NORETURN, etc.).
# tree-sitter consumes MACRO as the declarator
# identifier and emits the real param name as a
# sibling ERROR node. When (a) the parsed name is
# ALL-CAPS / underscored (looks like a macro), and
# (b) the next sibling is an ERROR containing one
# identifier, fold the macro into the type and use
# the ERROR identifier as the name.
if (
p_name
and _looks_like_macro(p_name)
and idx + 1 < len(siblings)
and siblings[idx + 1].type == "ERROR"
):
err_node = siblings[idx + 1]
err_text = _slice_bytes(src_bytes, err_node).strip()
if err_text and err_text.isidentifier():
p_type = f"{p_type} {p_name}".strip()
p_name = err_text
params.append((p_type, p_name))
elif child.type == "variadic_parameter":
params.append(("...", ""))
if not fn_name:
return None
# Return type: base type node + any pointer stars from the declarator
type_node = node.child_by_field_name("type")
ret_type = _slice_bytes(src_bytes, type_node).strip() if type_node else "unknown"
# Recover from tree-sitter's misparse of ``MACRO RealType fn(...)``
# where MACRO is an unknown storage-class macro (curl's ``UNITTEST``
# expands to ``static`` or empty; OpenSSL's ``OSSL_API`` adds
# visibility; Linux's ``__init`` / ``__cold`` are linker-section
# annotations). tree-sitter consumes MACRO as the ``type`` field
# and stashes ``RealType`` in a sibling ERROR node. When (a) the
# parsed type looks like a macro (ALL_CAPS-with-underscore or
# leading ``__``) and (b) one of the function_definition's
# children is an ERROR containing a single identifier, fold the
# macro into the type prefix and use the ERROR identifier as
# the actual return type. Regression: curl/urlapi.c run 2026-05-19
# produced ``UNITTEST result = parse_port(...)`` harness lines
# that CBMC rejected with "expected constant expression".
if type_node is not None and _looks_like_macro(ret_type):
for c in node.children:
if c.type == "ERROR":
err_text = _slice_bytes(src_bytes, c).strip()
if err_text and err_text.isidentifier():
ret_type = f"{ret_type} {err_text}"
break
# Recover from tree-sitter's misparse of ``MACRO struct T * fn(...)``.
# When an unknown macro prefixes the signature (GGML_API, EXPORT,
# __attribute__((...)), β¦), tree-sitter consumes ``MACRO struct`` as
# a stray declaration and the function_definition's ``type`` field
# picks up only the ``T`` half β yielding ``T *`` instead of
# ``struct T *``. CBMC then rejects the harness because the bare
# tag is not a valid type without a typedef.
#
# Look at the source bytes immediately before type_node.start_byte:
# skip whitespace, then check whether the preceding token is one of
# ``struct``, ``union``, ``enum``. If so, prepend it. We stop at the
# nearest statement separator (``;``, ``{``, ``}``) so a struct/
# union keyword from an UNRELATED prior declaration is not picked up.
if type_node is not None:
prepend = _recover_struct_keyword(src_bytes, type_node.start_byte)
if prepend and not ret_type.split()[:1] == [prepend]:
ret_type = f"{prepend} {ret_type}"
if pointer_stars:
ret_type = ret_type + " " + pointer_stars
is_static = "static" in ret_type.split()
return FunctionSignature(name=fn_name, return_type=ret_type, parameters=params, is_static=is_static)
_STRUCT_TAG_KEYWORDS = (b"struct", b"union", b"enum")
def _looks_like_macro(name: str) -> bool:
"""Heuristic: identifier is a parameter-qualifier or storage-class
macro (not a real parameter name or type).
True when the identifier is:
* all-uppercase with at least one underscore (``GGML_RESTRICT``,
``OSSL_API``), OR
* a single all-uppercase word of >= 4 chars (``UNITTEST``,
``EXPORT``, ``INLINE``), OR
* begins with a double underscore (``__restrict__``,
``__nonnull__``, ``__attribute__``).
Used to detect tree-sitter's misparse of ``MACRO RealType name``
(return type) or ``T * MACRO name`` (param) so the qualifier is
folded into the type and the real identifier is recovered from a
sibling ERROR node. Conservative β single-letter caps (``T``
template-style) and 2-3 letter words (``OK``, ``NO``) do not match.
"""
if not name:
return False
if name.startswith("__"):
return True
if "_" in name and name.isupper():
return True
if len(name) >= 4 and name.isupper() and name.isalpha():
return True
return False
def _recover_struct_keyword(src_bytes: bytes, type_start: int) -> str:
"""Return ``struct`` / ``union`` / ``enum`` if it precedes the type
in the source text (separated only by whitespace and statement-
local tokens), else ``""``.
Used to repair the tree-sitter misparse where a macro prefix
(GGML_API, EXPORT) causes the parser to consume the struct keyword
as part of a stray declaration. We scan backwards from ``type_start``
over whitespace, then check whether the next preceding token is
one of struct/union/enum. We stop at ``;``, ``{``, or ``}`` so
keywords from an unrelated earlier declaration are not picked up.
"""
i = type_start - 1
# Skip whitespace
while i >= 0 and src_bytes[i:i + 1] in (b" ", b"\t", b"\n", b"\r"):
i -= 1
if i < 0:
return ""
# Stop at statement boundaries β don't claim a struct keyword from
# an unrelated earlier declaration.
if src_bytes[i:i + 1] in (b";", b"{", b"}", b")", b"("):
return ""
# Walk back to the start of the preceding identifier-like token.
end = i + 1
while i >= 0 and (
src_bytes[i:i + 1].isalpha() or src_bytes[i:i + 1].isdigit() or src_bytes[i:i + 1] == b"_"
):
i -= 1
start = i + 1
token = src_bytes[start:end]
if token in _STRUCT_TAG_KEYWORDS:
return token.decode("ascii")
return ""
def _extract_param_ts(param_node, src_bytes: bytes) -> tuple[str, str]:
"""Return (type_str, name_str) from a parameter_declaration node.
Handles three declarator shapes that put non-identifier punctuation
onto the "name" half of a whitespace split:
* pointer prefix: ``T *p`` β type=``T*`` name=``p``
* double pointer: ``T **pp`` β type=``T**`` name=``pp``
* array decay: ``T buf[N]`` β type=``T*`` name=``buf``
(the array size is lost β that's correct for C, where array
parameters decay to pointers at the call site; a downstream
harness that emits ``buf[N]`` from this string would be passing
an element, not the array. ch341/pl2303 sweep regression.)
"""
full_text = _slice_bytes(src_bytes, param_node).strip()
parts = full_text.rsplit(None, 1)
# Handle ``void*param`` / ``int**pp`` / etc. β no whitespace between
# the pointer stars and the identifier. ``rsplit(None, 1)`` then
# returns a single element and the name is lost. Detect and split.
if len(parts) == 1:
m = re.match(
r"^(.+?)\s*(\*+)([A-Za-z_]\w*)\s*$",
full_text,
)
if m:
parts = [m.group(1).strip() + m.group(2), m.group(3)]
if len(parts) == 2:
last = parts[1]
# Strip leading pointer stars from the name; they belong on the
# type. ``**pp`` β name=``pp`` + 2 trailing stars on type.
name = last.lstrip("*")
stars = "*" * (len(last) - len(name))
type_str = parts[0].strip() + stars
# Array-decay: ``buf[N]`` (or ``buf[]``) on the name half means
# the parameter is logically a pointer. Strip ``[...]`` from
# the name and add one ``*`` to the type. Multi-dimensional
# arrays (``buf[N][M]``) also decay β first dim only becomes a
# pointer; later dims stay as part of the type.
if "[" in name:
bracket = name.index("[")
tail = name[bracket:]
name = name[:bracket]
# First ``[...]`` decays to ``*``; any remaining brackets
# stay on the type. ``buf[N][M]`` β name=``buf``,
# type=``T (*)[M]`` (we approximate as ``T*[M]`` because the
# downstream harness gen doesn't currently use the inner
# dimension and the value-arg form is what matters).
first_close = tail.find("]")
remainder = tail[first_close + 1:] if first_close >= 0 else ""
type_str = type_str + "*" + remainder
return type_str, name
return full_text, ""
def _collect_calls_ts(node, callees: set[str], src_bytes: bytes) -> None:
"""Recursively collect function call names from a tree-sitter subtree."""
if node.type == "call_expression":
fn_node = node.child_by_field_name("function")
if fn_node:
name = _slice_bytes(src_bytes, fn_node).strip()
callees.add(name)
for child in node.children:
_collect_calls_ts(child, callees, src_bytes)
# ---------------------------------------------------------------------------
# Regex fallback
# ---------------------------------------------------------------------------
# Matches C function definitions (handles pointers, multi-word return types)
_FUNC_DEF_RE = re.compile(
r"""
(?:^|\n) # start of line
(?P<ret>[\w\s\*]+?) # return type (non-greedy)
\s+
(?P<name>[A-Za-z_]\w*) # function name
\s*\( # opening paren
(?P<params>[^)]*) # parameter list
\)\s*\{ # closing paren + opening brace
""",
re.VERBOSE | re.MULTILINE,
)
_CALL_RE = re.compile(r"\b([A-Za-z_]\w*)\s*\(")
_KEYWORDS = frozenset(
[
"if", "else", "while", "for", "do", "switch", "case", "return",
"sizeof", "typeof", "alignof", "alignas", "static", "extern",
"inline", "const", "volatile", "struct", "union", "enum",
"typedef", "void", "int", "long", "short", "char", "float",
"double", "unsigned", "signed",
]
)
def _parse_with_regex(source: str, path: str) -> ParsedCFile:
"""Fallback regex-based C parser."""
functions: dict[str, FunctionSignature] = {}
call_graph: dict[str, set[str]] = {}
function_bodies: dict[str, str] = {}
function_definitions: dict[str, str] = {}
matches = list(_FUNC_DEF_RE.finditer(source))
for i, m in enumerate(matches):
fn_name = m.group("name")
if fn_name in _KEYWORDS:
continue
ret_type = m.group("ret").strip()
raw_params = m.group("params").strip()
params = _parse_params_regex(raw_params)
is_static = "static" in ret_type.split()
# Extract body: from { to matching }
body_start = m.end() - 1 # points at the '{'
body_text = _extract_body(source, body_start)
body_end = body_start + len(body_text)
functions[fn_name] = FunctionSignature(
name=fn_name,
return_type=ret_type,
parameters=params,
is_static=is_static,
)
function_bodies[fn_name] = body_text
function_definitions[fn_name] = source[m.start():body_end]
# Collect calls within the body
callees: set[str] = set()
for cm in _CALL_RE.finditer(body_text):
callee = cm.group(1)
if callee not in _KEYWORDS:
callees.add(callee)
call_graph[fn_name] = callees
address_taken_in = _compute_address_takers(functions, function_bodies)
return ParsedCFile(
path=path,
functions=functions,
call_graph=call_graph,
function_bodies=function_bodies,
function_definitions=function_definitions,
address_taken_in=address_taken_in,
)
def _compute_address_takers(
functions: "dict[str, FunctionSignature]",
function_bodies: "dict[str, str]",
) -> dict[str, set[str]]:
"""For each function in *functions*, find which OTHER functions' bodies
contain its identifier in a non-call position.
Returns a dict ``address_taken_in[name] = {caller1, caller2, ...}``
where each entry means "caller's body mentions name without an
immediately-following ``(``".
Used by Phase 3's caller-feasibility check: a function whose
address is taken (passed to a registration function, stored in a
vtable struct field, used as a callback) is reachable through
that indirection even though direct-call search returns nothing.
Conservative β picks up some non-callback references too (e.g. a
function name appearing in a comment, in a #define expansion, or
cast to a pointer for diagnostic purposes). Those false-positives
are harmless for Phase 3's "is the function reachable at all"
question.
Cheap to compute: one body-scan per function, with the name set as
a precomputed regex alternation.
"""
if not functions or not function_bodies:
return {}
fn_names = list(functions.keys())
if not fn_names:
return {}
# Build a regex that matches any function name followed by a
# non-call context (anything other than ``(``, possibly with
# whitespace).
name_alt = "|".join(re.escape(n) for n in fn_names)
# ``\b(NAME)\b(?!\s*\()`` β match the bare identifier, NOT followed
# by optional whitespace + ``(``.
pat = re.compile(rf"\b({name_alt})\b(?!\s*\()")
result: dict[str, set[str]] = {}
for caller_name, body in function_bodies.items():
if not body:
continue
for m in pat.finditer(body):
target = m.group(1)
if target == caller_name:
# Self-reference (recursion mark, etc.) β not an
# address-take we care about for cross-function flow.
continue
result.setdefault(target, set()).add(caller_name)
return result
def _parse_params_regex(raw: str) -> list[tuple[str, str]]:
"""Parse a raw parameter string into [(type, name), ...] pairs."""
if not raw or raw.strip() in ("", "void"):
return []
params: list[tuple[str, str]] = []
for part in raw.split(","):
part = part.strip()
tokens = part.split()
if len(tokens) >= 2:
last = tokens[-1]
name = last.lstrip("*")
stars = "*" * (len(last) - len(name))
typ = " ".join(tokens[:-1]) + stars
params.append((typ, name))
elif tokens:
params.append((tokens[0], ""))
return params
def _extract_body(source: str, open_brace: int) -> str:
"""Extract the text from the opening brace to its matching closing brace."""
depth = 0
i = open_brace
while i < len(source):
ch = source[i]
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return source[open_brace: i + 1]
i += 1
return source[open_brace:]
|