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 type | Applies? | Depth |
|---|---|---|
| SAST | ✅ | Dedicated 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). |
| SCA | ✅ | Conan lockfiles (see languages.md). |
| Secrets | ✅ | Language-agnostic. |
| Config | ✅ | Language-agnostic. |
| API | ✅ | OpenAPI/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:
cif (len < cap) { memcpy(dst, src, len); } // (a) enclosing guard — suppressedif (n > MAX) return; /* ... */ memcpy(buf, src, n); // (b) early-exit guard — suppressedsize_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:
| Source | Tainted | Tier |
|---|---|---|
recv, recvfrom, read | buffer arg | Remote |
getenv | return value | Env |
fgets, gets, fread, getline, getchar, fgetc | buffer/return | Filesystem |
scanf, fscanf, sscanf | buffer arg | LocalCli |
argv parameter | parameter | LocalCli |
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:
| Class | Members | Non-remote source is… |
|---|---|---|
CommandExec | system, popen, execl…, execv…, posix_spawn… | still dangerous (setuid escalation) — stays visible |
PathOrMemory | fopen, 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
| Class | Sink shape | Notes |
|---|---|---|
| Buffer overflow | memcpy(dst, src, len), memmove, read, fread | rule requires the tainted length arg (a tainted destination pointer alone is benign) |
| Alloc-size | malloc(n), calloc(a, b), realloc(p, n), alloca(n) | length-arg positions are enumerated per sink |
| Format string | printf(fmt), sprintf, snprintf, syslog, fprintf | v-family (vprintf, …) has a transparent-forwarder guard |
| Command exec | system(cmd), popen, execve | CommandExec class — non-remote still gates |
| Path open | fopen(path), open | PathOrMemory class — non-remote may demote |
cvoid handle(int fd) {char buf[64];ssize_t n = recv(fd, netbuf, sizeof netbuf, 0); // Remote sourcememcpy(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_ccsidSSRF, thelib/rand.cternary-min,lib/curl_gssapi.cfield-name), sqlite (idxPrepareStmt,sqlite3_rsync.cfield-write chaining), libpng (safe_readself-reference), libssh (misc.cstrcpy 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
PathOrMemoryflows 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
#definealiases; function-like macros, multi-token bodies, and#ifdefare 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-
mainpruning at the front-end. - Sink coverage is rule-defined, not engine-defined.
Related documentation
- 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
SSLKEYLOGFILEcase, and the FP-prevention program. - tiers.md — the three-tier model and
clang-tidyTier-1 corroboration via--corroborate.
