GCA · LANGUAGE GUIDES

    C & C++

    Deep-dive: the dedicated precision engine — Gadriel Code's deepest analysis.

    C & C++ — Deep-Dive Guide

    C and C++ get Gadriel Code's dedicated precision engine — the most carefully gated SAST front-end in the scanner. This is deliberate: raw name-matching on C is historically high-FP, so the engine is built around positive-proof gating (compile-DB scoping, AST-dominance guards, a sink taxonomy, provenance demotion, and macro-alias resolution), every layer of which is fail-open and recall-safe. This guide goes below the summary in languages.md; read that and scan-coverage.md first.

    C and C++ share one taint engine (taint_c/) because tree-sitter-c and tree-sitter-cpp share the core node kinds. The front-ends (c.rs, cpp.rs) differ only in C++-specific shapes (qualified_identifier, new_expression, templates).


    Which scans apply, and how deep

    Scan typeApplies?Depth
    SASTDedicated precision engine — intra-file, interprocedural-within-file, tiered-provenance taint with AST-dominance guards, sink taxonomy, and compile-DB scoping. No cross-file taint (deferred).
    SCAConan lockfiles (see languages.md).
    SecretsLanguage-agnostic.
    ConfigLanguage-agnostic.
    APIOpenAPI/GraphQL.

    The precision engine

    Compile-DB scoping

    If a compile_commands.json is present, the engine scopes analysis to the real translation units of the build. It is searched at root/, root/build/, root/out/, then ancestor directories, so a cmake -B build layout works without configuration. A C/C++ source file (.c, .cc, .cpp, .cxx, .cu) is skipped only when the compile DB proves it is not part of the build; headers are never skipped (they are #included into TUs). Every absence is fail-open: no DB, a parse error, or a non-source extension means nothing is skipped, so behaviour without a compile DB is identical to before.

    AST-dominance guard dataflow

    Buffer/alloc/length findings are suppressed only on positive structural proof that a dominating relational guard bounds the tainted length. Over the tree-sitter AST, the engine finds the enclosing function and looks for an if whose condition relationally bounds (< <= > >=; ==/!= bound nothing) the tainted identifier, in one of two dominance shapes:

    if (len < cap) { memcpy(dst, src, len); } // (a) enclosing guard — suppressed
    if (n > MAX) return; /* ... */ memcpy(buf, src, n); // (b) early-exit guard — suppressed
    size_t m = elen < clen ? elen : clen; // ternary MIN/MAX clamp — suppressed

    Early-exit kinds are return, break, goto, continue. Any uncertainty — no enclosing function, a guard that does not mention the identifier, an unresolvable AST — fails open and the finding fires. The language-agnostic machinery is shared with the Go guard analyzer; only the C relational-bound predicate and early-exit kinds are C-specific.

    Macro-alias resolution

    Without a preprocessor, #define SYSTEM system used to hide a SYSTEM(data) call from name-matching. The engine recovers object-like macro aliases directly from tree-sitter preproc_def nodes (single-identifier name→value only, transitive, cycle-safe to depth 8). This is a correctness fix, not a heuristic, and it is bidirectionally sound: #define system safe_wrapper correctly stops the system rule from firing. (Applied in the C front-end and the shared taint engine; the C++ front-end does not apply it in build_call_node.)

    Sources it recognizes, and their provenance

    Every source carries a provenance tier:

    SourceTaintedTier
    recv, recvfrom, readbuffer argRemote
    getenvreturn valueEnv
    fgets, gets, fread, getline, getchar, fgetcbuffer/returnFilesystem
    scanf, fscanf, sscanfbuffer argLocalCli
    argv parameterparameterLocalCli

    The argv seed is strict: the parameter must be named argv/av, be a char **/char *[], to avoid tainting common OUT-param shapes (stringp, pzErrmsg) — a fix for named FPs in curl and sqlite.

    Crucially, getenv and argv are non-remote. has_remote_bound() is true only for recv/recvfrom/read; tiers are preserved verbatim across interprocedural hops (a getenv value does not upgrade to Remote through a call).

    The sink-class taxonomy

    Sinks are split into two provenance-risk classes:

    ClassMembersNon-remote source is…
    CommandExecsystem, popen, execl…, execv…, posix_spawn…still dangerous (setuid escalation) — stays visible
    PathOrMemoryfopen, open, openat, freopen, malloc, calloc, realloc, memcpy, memmove, memset, bcopy, …genuinely low-risk when the source is non-remote — may demote

    Classification strips any :: namespace (std::memcpy → PathOrMemory) and is fail-open: an unrecognized sink defaults to CommandExec (keep-visible).

    This is what fixes the canonical SSLKEYLOGFILE false positive (described in false-positives.md §3.1): fopen(getenv("SSLKEYLOGFILE")) is an all-PathOrMemory sink with an Env source, so it demotes; whereas system(argv[1]) in a setuid binary stays gating because system is CommandExec.

    Sink families and finding classes

    ClassSink shapeNotes
    Buffer overflowmemcpy(dst, src, len), memmove, read, freadrule requires the tainted length arg (a tainted destination pointer alone is benign)
    Alloc-sizemalloc(n), calloc(a, b), realloc(p, n), alloca(n)length-arg positions are enumerated per sink
    Format stringprintf(fmt), sprintf, snprintf, syslog, fprintfv-family (vprintf, …) has a transparent-forwarder guard
    Command execsystem(cmd), popen, execveCommandExec class — non-remote still gates
    Path openfopen(path), openPathOrMemory class — non-remote may demote
    void handle(int fd) {
    char buf[64];
    ssize_t n = recv(fd, netbuf, sizeof netbuf, 0); // Remote source
    memcpy(buf, netbuf, n); // buffer overflow — fires (no dominating guard)
    }

    Copy-propagators (strcpy, strncpy, memcpy, sprintf, snprintf, …) carry taint from source to destination arg.

    Sanitizer / guard recognition

    Beyond the AST-dominance length guard, the engine recognizes: rule sanitizer_functions: in the sink's own arguments; chain-terminating sanitizers; constexpr length slots (sizeof/macro/literal cannot be attacker-controlled); and a str-copy safe-sizing guard that drops strcpy(dst, src) when dst = malloc(strlen(src)+k) (a real FP fixed against libssh).


    No cross-file taint

    Unlike Python, JS/TS, and Go, C/C++ cross-file/workspace propagation is deferred. The workspace pass has no C/C++ bucket, and #include analysis is not used for taint. C/C++ taint is strictly intra-file, with interprocedural call-following within the same file (a per-file function name index; a bare system/printf with no in-file definition simply seeds nothing). The languages.md "✅ (precision engine)" row is about depth of the intra-file analysis, not cross-file reach.


    Tier-1 corroboration with clang-tidy

    C/C++ is where the Tier-1 model started. With --corroborate (opt-in, off by default), Gadriel drives your installed clang-tidy — using compile_commands.json when available (-p <build-dir>), else standalone — with clang-analyzer-*, bugprone-*, and cert-* checks. Results are mapped to CWEs (null-deref→476, use-after-free→416, format-string→134, insecureAPI.strcpy→120, …), tagged Tier1External at Medium confidence, and merged additively: overlaps stamp corroborated_by, and disjoint clang-tidy findings are appended. Corroboration never changes a finding's severity or confidence; it is read-only and advisory.

    How much clang-tidy actually contributes is environment-dependent and experimental: without a compile_commands.json, clang-tidy often can't resolve includes and may return nothing, so the merge can be a no-op. The corroboration mechanism is solid (it works strongly for Go/gosec); treat C clang-tidy lift as best-effort, not guaranteed.


    Honest scope: C SAST is historically high-FP and heavily gated

    This is the honest headline. Raw name-matching on C produces a very high false positive rate, so the engine is built around suppressing FPs only on positive proof, one documented upstream FP at a time. The evidence is in-tree:

    • The language-agnostic substring / one-hop heuristics are skipped entirely for C/C++ — those languages must be satisfied by the real CFG taint analyzer only, precisely because the substring check is a C/C++ FP factory.
    • Named FP gates cite real projects: curl (curl_easy_setopt_ccsid SSRF, the lib/rand.c ternary-min, lib/curl_gssapi.c field-name), sqlite (idxPrepareStmt, sqlite3_rsync.c field-write chaining), libpng (safe_read self-reference), libssh (misc.c strcpy sizing), zstd/mongoose.
    • Field-writes (p->field = tainted) are tracked intra-procedurally but not forwarded across functions, to avoid multi-hop field-chaining FPs.
    • Provenance demotion moves non-remote PathOrMemory flows out of the gate (see below).

    Because every gate suppresses only on positive proof and fails open, precision is bought carefully without trading recall — but the flip side is that C/C++ findings depend heavily on the GVL rule corpus and the gates above, and C carries a NEEDS_RECALL_ANCHOR flag in the benchmark program (see false-positives.md §6).

    Provenance demotion (getenv / argv are non-remote)

    A C/C++ finding is demoted out of the gating stream (its confidence set to Low, which the gate reclassifies as advisory) only when all hold: the provenance verdict says demote; the dominant source tier is not Remote; the rule targets C/C++; and every sink in the rule is PathOrMemory. Severity is retained for audit — the finding stays in findings.json with provenance: {tier, demoted: true}. So fopen(getenv(...)) demotes, but system(argv[1]) (a CommandExec sink) keeps gating.

    Other limits

    • No cross-file taint (deferred), as above.
    • No preprocessor beyond single-identifier object-like #define aliases; function-like macros, multi-token bodies, and #ifdef are not evaluated.
    • No type/overload resolution — name resolution is textual/structural; C++ overloading is not modeled.
    • Argument shapes are literals-only — every non-literal is Unknown (shape predicates fail closed).
    • Entry-point policy is "every function is analyzed" — there is no reachable-from-main pruning at the front-end.
    • Sink coverage is rule-defined, not engine-defined.

    • languages.md — the cross-language capability matrix.
    • scan-coverage.md — the six scan types and eight pillars.
    • false-positives.md — confidence, effective risk, the provenance taxonomy, the SSLKEYLOGFILE case, and the FP-prevention program.
    • tiers.md — the three-tier model and clang-tidy Tier-1 corroboration via --corroborate.