Issue #37 ports the sixteen scored Java propagation templates to C++. The C++
cases keep the Java template_id values, source-to-sink polarity, and negative
mechanism; only the smallest fixture construct changes to C++ syntax. Every
scored C++ template has exactly one positive and one negative core case,
so the C++ core denominator is 16 templates and 32 assertions, exactly as the
applicability matrix fixes it.
C++ and C are ported together because they share CodeQL's cpp
extractor and the same struct-based heap adaptations, but they are two
populations: C's denominator is 15 templates and 30 assertions, and the two
are never merged, pooled, or macro-averaged together.
The matrix classifies the local, calls/returns, and control-transfer strata as
directly applicable — including dfb-template-exception-catch, which throws a
class instance and catches it by reference with the value carried in a member —
and language-adapts four heap cells. This contract records no deviation from
the matrix.
| Stratum | Template ID | C++ adaptation |
|---|---|---|
| Local | dfb-template-direct-propagation |
Direct free-function call, unchanged in meaning. |
| Local | dfb-template-local-overwrite-kill |
A local int is either reassigned from a copy that preserves the value or reassigned to a constant before the sink. |
| Local | dfb-template-local-multi-step-chain |
Local int copies carry the value through the same three-step chain. |
| Local | dfb-template-arithmetic-expression-propagation |
C++ integer arithmetic preserves the expression-flow distinction. |
| Calls/returns | dfb-template-call-context-separation |
One relay function is called with tainted and clean values; the selected call remains the distinction. |
| Calls/returns | dfb-template-argument-position-separation |
choose_first returns its first parameter, so the second-argument negative remains non-flowing. |
| Calls/returns | dfb-template-return-relay-one-hop |
A one-hop function return carries the value to the sink. |
| Calls/returns | dfb-template-return-relay-two-hop |
Two nested function returns preserve the two-hop depth. |
| Heap/separation | dfb-template-object-separation |
Two Holder struct objects with the same member name stand in for the distinct Java objects. |
| Heap/separation | dfb-template-same-object-field-separation |
One Holder has separate tainted and clean members. |
| Heap/separation | dfb-template-alias-propagation-separation |
A reference alias (Holder &alias = original) aliases the same object while a second object stays distinct. |
| Heap/separation | dfb-template-array-element-separation |
A native int values[2] with distinct constant indices stands in for the Java array. |
| Control transfer | dfb-template-infeasible-branch |
Literal true/false conditions make the positive/negative path feasible or unreachable. |
| Control transfer | dfb-template-branch-join |
The negative overwrites the value on both branches; the positive leaves one path tainted. |
| Control transfer | dfb-template-loop-carried-kill |
A for loop either overwrites the carried value or computes from it. |
| Control transfer | dfb-template-exception-catch |
A FlowException struct carries the value in a member across throw / catch (FlowException &caught), matching the Java checked-exception construct. |
Since the challenge-tier expansion below, the C++ core denominator is 28 templates and 56 assertions. The paragraph above describes the classic sixteen-template kernel, which remains exactly as published; the expanded denominator is a different population and the two are never compared number-to-number.
All C++ fixtures use the benchmark-controlled dfb_source and dfb_sink
function names. Fixtures are single .cpp files with no header, no build file,
and no external dependency; each compiles standalone under clang++ -std=c++17.
Adapters may lower those endpoints through their own models, but the case
metadata stays analyzer-neutral and reports retain only observed evidence.
The C++ population is the 56 taint/core cases under cases/taint/cpp/.
Thirty of them were authored for this kernel with
fixture_provenance.revision m2-cpp-kernel, and 24 for the challenge-tier
expansion with revision m3-challenge-cpp. The direct-propagation pair
(dfb-taint-cpp-direct-positive and dfb-taint-cpp-direct-negative) predates
it: it is the C++ member of the 13-language direct-flow breadth slice, and it is
frozen byte-for-byte in the published v0.2.0 manifest (reports/freeze.json).
Its case.json therefore keeps fixture_provenance.revision m1a-direct-core,
keeps the breadth policy reference
adapters/bifrost/policies/core-direct.rqlp, and carries no CodeQL model
reference.
Editing those two files would invalidate the published v0.2.0 evidence, so the
runners accommodate them exactly as the Kotlin and C# kernels do: the Bifrost
selector accepts either core-cpp-kernel.rqlp or the breadth
core-direct.rqlp policy and evaluates each case through the policy it
declares, and the CodeQL selector accepts a C++ core case with no codeql model
reference while rejecting one that names a different query. The same case is a
member of two populations, but its results are never pooled: the breadth result
lives in reports/bifrost-smoke.json and the kernel result in the dedicated C++
reports below.
The Bifrost C++ slice uses the language-qualified policy
adapters/bifrost/policies/core-cpp-kernel.rqlp, whose source and sink
selectors are (language cpp (call :callee (name "dfb_source"))) and
(language cpp (call :callee (name "dfb_sink"))), with argument index 0 as the
dangerous operand. Run it from the repository root:
cargo run -- run-bifrost-cpp-kernel --bifrost /path/to/bifrostThe command selects only the C++ core assertions — 56 since the challenge row
flipped — materializes one isolated
workspace per case outside the repository, writes the normalized report to
reports/bifrost-cpp-kernel.json, and retains the verbatim per-case Bifrost
JSON under reports/raw/bifrost-cpp-kernel/. A report with incomplete runs is
normalized as inconclusive, never as a negative.
The CodeQL C++ vertical slice is exactly that population — 56 cases since the challenge expansion, though the retained report predates it. Every selected case is analyzed with the dedicated query:
adapters/codeql/cpp/queries/CppKernel.ql
The query is owned by the shared C-family pack manifest at
adapters/codeql/cpp/qlpack.yml, pinned to codeql/cpp-all@12.0.2 — the
version codeql pack install resolves for CodeQL CLI 2.26.4 — with the full
transitive set committed in adapters/codeql/cpp/codeql-pack.lock.yml. One pack
holds both C-family queries because CodeQL extracts C and C++ with one cpp
extractor and one standard library; the populations stay disjoint through three
independent guards, exactly as JavaScript and TypeScript do:
- the runner selects only its own language's cases and refuses a case that declares the other kernel's query;
- each query restricts every data-flow node to its own fixture extension
(
cpphere,cinCKernel.ql); - the two runs write separate reports and separate raw-evidence roots.
Registry retrieval of the C-family pack succeeded for the pinned CLI, so no source workspace fallback was needed:
codeql pack install adapters/codeql/cpp
cargo run -- run-codeql-cpp-kernel --codeql /path/to/codeqlIf registry retrieval is unavailable, a matching official source workspace or
CLI bundle pack root passed through --codeql-packs is a valid reproduction
input, as documented for the JavaScript kernel.
For each case the runner creates one cold cpp database from the declared
fixture file only, runs the dedicated query, and removes the temporary workspace
and database after retaining the evidence. Databases are created with
--build-mode=none, which CodeQL 2.26.4 supports for C and C++: the buildless
extractor indexes the fixture and resolves the translation unit through a
compiler it discovers on the host (Apple clang 21.0.0 for the retained run). No
--command= build is traced, and the fixtures need no makefile or project file.
The normalized report is reports/codeql-cpp-kernel.json and the raw SARIF (or
raw runner diagnostics when CodeQL cannot produce SARIF) is retained per case
under reports/raw/codeql-cpp-kernel/.
CodeQL query results are evidence, not ground truth by themselves. The runner
reconciles SARIF result locations with the case's DFB-SINK: anchor. That
marker identifies the anchored sink function declaration; the C-family dialect
of the shared reconciler reads the declared function name as the identifier
preceding the parameter list, and then accepts a SARIF result that lies in the
same fixture file on a line that calls that function. A line that reaches a
member through ., ->, or :: is not such a call. The result need not be on
the marker's own line. Query path evidence identifies the DFB-SOURCE: to sink
flow, and normalized results retain both anchor sets.
A successful, anchor-backed finding is reached; a successful analysis with no
matching finding is not-reached. Missing, ambiguous, or unmappable location
evidence is inconclusive, an explicitly unsupported capability is
unsupported, and a database, query, SARIF, or runner failure is
runner-error.
None of inconclusive, unsupported, or runner-error may be normalized to
not-reached, and none may be counted as a semantic negative. This keeps
execution health separate from the polarity of the 16 balanced assertions.
Both retained snapshots cover the classic 32 C++ core assertions — they were taken before the challenge expansion and are freeze-bound, so neither was re-run by the challenge wave (see the challenge-tier expansion below). They are separate populations and are not pooled with each other, with the C kernel, or with any other language, and neither is compared with an expanded-core number.
CodeQL CLI 2.26.3, build codeql-cli:7d097a43199effe04ecd9c6bd3ad9bb02a45b3d7,
with codeql/cpp-all@12.0.2 from the committed lock and Apple clang 21.0.0
(clang-2100.1.1.101) as the compiler the buildless extractor discovered.
Configuration hash
8873a63a5898c8b6b10dc24a9fbf2fae3ed5a088faf024524b0bae50f0fc4cc0.
32 results: 16 reached and 16 not-reached, with zero inconclusive,
unsupported, or runner-error outcomes. 28 of 32 match the expected polarity.
The four mismatches are:
- false negatives:
dfb-taint-cpp-alias-propagation-positiveanddfb-taint-cpp-exception-catch-positive; - false positives:
dfb-taint-cpp-array-element-negativeanddfb-taint-cpp-loop-carried-negative.
That set is a subset of the five mismatches the Java and C# kernels show on the same templates with the same CLI: the C++ extractor resolves the arithmetic-expression positive that those two miss. The alias-propagation and exception-catch false negatives and the array-element and loop-carried false positives are the same cells that fail across every ported kernel.
All 32 retained raw outputs are SARIF files under
reports/raw/codeql-cpp-kernel/, with zero error files, and normalized
witness_checkpoints are empty for every case: the adapter records
anchor-backed flow outcomes and leaves the path evidence in SARIF rather than
fabricating observed witness markers. Per-case wall clock, including cold
database creation, ranged from 6.2 s to 24.8 s (306 s for the population). The
60-second execution_budget on the cases describes the analysis budget shared
with the other language kernels; C++ extraction time is reported here rather
than silently rebudgeted.
Bifrost 0.10.5, build identity
728ac69ab93224151c6c951b23d2f5bc681d8558. Configuration hash
b29775f28c44e0830155def3030cb36f7c7f8906c440dc18af2be6f7ddbdc22e; it covers
both core-cpp-kernel.rqlp and the breadth core-direct.rqlp, because the
frozen direct pair is evaluated through the policy it declares.
32 results: 1 reached, 1 not-reached, and 30 inconclusive. Only the
direct-propagation pair is decisive, and both of its outcomes match the expected
polarity — 2 of 2 decisive outcomes, 2 of 32 assertions.
The 30 inconclusive results are capability evidence, never negatives. Each
retains partial_discovery with a diagnostic of the form "taint discovery is
incomplete: procedure value-flow snapshot for ... is unknown", naming the
fixture's dfb_source procedure. This matches the C# kernel's Bifrost profile
on the same build: Bifrost's C++ procedure value-flow coverage, not an artifact
of the language-qualified policy.
Twelve of the thirteen templates of the challenge-tier
preregistration have landed for C++. The preregistration's
C++ row classifies dfb-template-chal-reflective-invocation inapplicable,
so the expansion adds 12 templates / 24 assertions and the C++ core denominator
becomes 28 templates / 56 assertions, exactly as the preregistration's
expanded-denominator table fixes it. The new cases live under
cases/taint/cpp/<template>-{positive,negative}/ with ids
dfb-taint-cpp-<template>-<polarity>, score_tier core, and
fixture_provenance.revision m3-challenge-cpp.
Every fixture is a single self-contained .cpp file using only the C++
standard library (<string>, <map>, <vector>, <functional>). No header,
no build file, no third-party dependency — the same shape the classic kernel
ships. All 24 compile clean under the kernel's documented host check,
clang++ -std=c++17 (Apple clang 21.0.0), which is an authoring check only:
the CodeQL C++ runner still creates databases with --build-mode=none, so no
build step is part of any measurement.
dfb-template-chal-reflective-invocation has no C++ fixture and is not in
the C++ denominator. Standard C++ has no run-time reflection: no
standard-library facility resolves a member function from a string at run
time. The nearest construct — a std::map<std::string, ...> of callable
values — is dfb-template-chal-dispatch-table, and encoding the same fixture
under a second template_id would inflate the denominator without asking a
second question. Compile-time reflection (P2996) is not in the pinned language
standard and is not used. This exclusion reduces only C++'s denominator and no
other language's, and it is the reason C++'s expanded core is 28 templates
where Java's, JavaScript's, Python's, TypeScript's, and C#'s are 29.
Ten cells are directly applicable. The two language-adapted cells are the
ones the preregistration names for C++, and both are implemented exactly as it
prescribes:
| Template | Classification | C++ construction |
|---|---|---|
dfb-template-chal-computed-property |
adapted | C++ has no member access by run-time name, so the computed key indexes a std::map<std::string, std::string> through a non-constant local key variable: holder[key] = dfb_source(); dfb_sink(holder[key]);. The negative writes and reads two provably distinct constant keys ("payload" / "other"). The member-access flavor of the template is lost; the computed-key flavor is preserved. That loss is recorded here, as the preregistration requires, and it is why the case carries computed-access without reflective-dispatch — the C++ adaptation is not reflective, unlike Java's and C#'s. |
dfb-template-chal-dispatch-table |
direct | std::map<std::string, std::function<void(const std::string &)>> populated with two free functions, leak and drop; a non-constant key selects one and the selected function value is invoked with the tainted value. The negative selects drop, which ignores its argument and sinks a clean constant. |
dfb-template-chal-closure-capture |
direct | A lambda captures an enclosing local by copy and is returned as a std::function<void()>, invoked by the caller after the capturing frame has returned. The negative captures the clean local instead; the tainted local is still assigned from dfb_source() in both. |
dfb-template-chal-function-field |
direct | A Holder struct with a std::function<void(const std::string &)> fn member; two instances, one assigned a sinking lambda and one an argument-dropping lambda, with a separate invoke function reading the member and calling it. The negative passes the second holder (object-separation). |
dfb-template-chal-callback-registration |
direct | A Registry struct holding std::vector<std::function<void(const std::string &)>>; register_hook appends and a separate fire iterates the registrations and invokes each with the tainted value. Zero frameworks, twenty lines of standard library. |
dfb-template-chal-anonymous-implementation |
adapted | C++ has no anonymous classes, but a lambda's closure type is unnamed, so the fixture invokes a capture-less lambda through a declared std::function<void(const std::string &)> variable. That preserves "flow through the method of an unnamed implementation, invoked via its declared type"; capture-less is what keeps it distinct from closure-capture. Both the sinking and the argument-dropping implementation are constructed in both polarities, and the negative invokes the dropping one. |
dfb-template-chal-map-iteration |
direct | std::map<std::string, std::string> retrieved by a range-for over the map's entries, never by a keyed at/find. The negative iterates a second, disjoint map that never received the tainted value. |
dfb-template-chal-nested-access-path |
direct | Three nested structs give a.b.c.value; the negative reads the sibling a.b.c.other at the same depth. |
dfb-template-chal-element-object |
direct | std::vector<Item> of two objects with distinct constant indices; field-separation, following the precedent the classic dfb-template-array-element-separation pair sets in all thirteen languages. C++'s adaptation separates by index, not by object identity, so the cell keeps field-separation rather than object-separation. |
dfb-template-chal-deep-relay-chain |
direct | Six free functions relay1…relay6, no branching and no state, with the sink applied to relay1's returned result. The negative feeds the identical chain a clean constant while the dfb_source() call stays live. |
dfb-template-chal-recursive-carry |
direct | std::string carry(const std::string &value, int depth) recursing to a base case at depth == 0, invoked with 5. The negative overwrites the carried value with a clean constant at the base case (overwrite-kill). |
dfb-template-chal-context-pair-depth2 |
direct | The canonical Amendment A1 construction: helper returns its argument, wrapper calls it, and outer_tainted / outer_clean are the two distinct two-deep contexts. Both calls stay live in both fixtures; the positive sinks the tainted context's result, the negative the clean one's. |
No template proved unimplementable and no amendment is proposed by this wave.
| Adapter | This wave | Report |
|---|---|---|
| Semgrep CE 1.174.0 | Ran, whole population | reports/semgrep-cpp-kernel.json |
| Bifrost v0.10.5 | Deferred (freeze-bound) | reports/bifrost-cpp-kernel.json |
| CodeQL 2.26.4 | Deferred (freeze-bound) | reports/codeql-cpp-kernel.json |
| Joern 4.0.617 | No C++ slice exists | — |
| Infer v1.3.0 | Ran, whole population — new adapter (#82), landed after the wave, post-freeze | reports/infer-cpp-kernel.json |
Infer arrived after this wave and is C++'s second engine. The Infer
adapter (#82) runs the pinned v1.3.0 release's Pulse taint analysis over the
whole expanded 56-assertion core — the first benchmark-controlled
interprocedural engine evidence on C++. Its report is post-freeze and binds
nothing. 56 results: 19 reached, 37 not-reached, zero inconclusive,
unsupported, or runner-error; 47/56 match expected polarity, with all
nine mismatches false negatives and zero false positives — every
negative is clean, including the two path-sensitivity negatives Semgrep CE's
engine trips on. Five of the nine misses share one property: the flow
crosses standard-library machinery — std::function values
(closure-capture, callback-registration, dispatch-table), std::map
entries (map-iteration, computed-property) — which the unmodeled Pulse
engine does not follow. The other four repeat the C kernel's measured
families (arithmetic-expression drops in expression and loop-carried,
the unfollowed recursive-carry) plus the C++-only exception-catch
positive, whose value travels through a thrown exception. The contrast with
C — where the raw function-pointer dispatch-table flow is followed, and
over-approximated in the negative — localizes the C++ indirection misses to
the library types rather than to indirection as such. See
the Infer adapter notes.
Bifrost and CodeQL are both deferred, and both for the same reason.
reports/bifrost-cpp-kernel.json and reports/codeql-cpp-kernel.json are two
of the nineteen reports reports/freeze.json digest-binds for v0.3.0, so this
change must not overwrite either: expanded Bifrost and CodeQL evidence for
C++ is pending the v0.4.0 freeze-prep re-run, on the repository's established
re-run-at-freeze pattern. The retained reports above remain the valid
32-assertion classic snapshots and describe a different population from the
expanded one. Deferral is not absence of coverage: both engines cover C++, both
selectors already expect the full 56, and this wave simply had no freeze-legal
file to write the expanded results to.
Joern has no C++ slice, and this wave did not invent one. The pinned
joern-v4.0.617 distribution ships c2cpg, which handles C++ upstream, but
this repository has no JoernKernel variant for C++, no C++ Joern query, and
no reports/joern-cpp-kernel.json; adapters/joern/README.md records C++ as
"Available, not yet in scope". Standing up a language slice is its own change
with its own reproduction contract, not a side effect of landing fixtures. C++
therefore has no Joern evidence at any denominator, classic or expanded.
reports/semgrep-cpp-kernel.json. The whole 56-case population is selected and
balance-checked, and the bounded CE profile then decides what is scored, from
case metadata, before Semgrep is invoked.
| Stratum | Assertions | Scored | unsupported |
Polarity match (scored) |
|---|---|---|---|---|
| Classic (16 templates) | 32 | 14 | 18 | 12/14 |
| Challenge (12 templates) | 24 | 0 | 24 | n/a |
Whole-population outcome distribution: 9 reached, 5 not-reached, 42
unsupported, zero inconclusive, zero runner-error.
All twenty-four challenge assertions take the preregistered unsupported
partition, exactly as the challenge tier predicted: no
challenge template carries the intraprocedural feature tag, so none is inside
the documented CE local-taint profile, and each retains its own
*-unsupported.json capability-decision document naming the declared
capability and the boundary it falls outside, citing the preregistered
per-template rationale rather than the generic tag rule. The scored subset
therefore stays at 14 assertions and 12/14, unchanged from the classic run
— the two mismatches are still the infeasible-branch and loop-carried
negatives, the path sensitivity the pinned CLI sells as Pro. Comparing the
retained report before and after this expansion, not one of the 32 classic
outcomes moved. The partition was not adjusted for this expansion, and
twenty-four declined assertions are coverage, never twenty-four false
negatives.
The expanded report carries fixture revision
sha256:a1570fc74526f0088488e3fba0941a7da47244635d7ceecf6787f1f76200b4ee,
configuration hash
865d0bd2989f9ddd0b90f2d6675584e86706b109a033d4a1ac00bd21a617b100, tool build
identity semgrep-oss:1.174.0. C++'s front end is recorded alpha in the
pinned distribution and that label is retained on every assertion. Reports at
different fixture revisions are not pooled.
Read plainly: the C++ challenge strata have no analysis outcomes. The only adapter that ran over the expanded population declined every challenge assertion by declared capability, which is a statement about Semgrep CE's documented boundary and not a measurement of C++ data-flow difficulty. Nothing here establishes how well any engine follows C++ taint through dispatch tables, lambdas, containers, or depth. That evidence arrives with the v0.4.0 re-run of the two deferred adapters.
The C++ challenge cases are excluded from the Bifrost smoke population by template identity, so the frozen 118-case smoke slice is untouched.
C++ results are their own population. They are never pooled with the C kernel
(whose denominator is 15 classic templates), with the Java, Kotlin, C#,
JavaScript, TypeScript, or Python kernels, or with the 13-language direct-flow
breadth slice, and they are never averaged with a language whose core
denominator is not also 28 templates — nor with a C++ score taken over the
classic 16, which is a different population of the same name. The Java
calibration cases
(dfb-template-one-hop-relay and dfb-template-modeled-external-summary) have
no C++ member and do not change this denominator.