RFC: Structured verification results (export-json) - #4727
Conversation
|
sorry for spamming, but this would help a lot with automatization and it seems previous PR stalled |
feliperodri
left a comment
There was a problem hiding this comment.
Thanks, thanks, thanks! Thanks for writing this up! This is a genuinely well-constructed RFC, and I appreciate that the motivation is grounded in things that actually exist in the tree rather than hypotheticals. I checked the claims and they hold up: the TODO at kani-driver/src/cbmc_property_renderer.rs:190, update_properties_with_reach_status at cbmc_property_renderer.rs:624, the --output-format=old mocking path at call_cbmc.rs:92, the SARIF writer skipping covers and successes (sarif.rs:143 and sarif_level() at sarif.rs:233), and the kebab-case keys in list/output.rs:113. The exhaustive property bucketing, the atomic-write contract, and the "null never means a guess" discipline are all more carefully specified than most RFCs in this directory.
I'm not asking you to change the design. What I'd like resolved before merge is interface
completeness and one piece of sequencing:
- The
--sarifalternatives section needs to argue on different grounds — see my inline comment. - The enum domains (
outcome.kind,verdict,failure_kind) need to be specified, not just shown by example. schema_versionneeds a compatibility policy and stabilization criteria attached to it.- Process: #4472 still carries its own
rfc/src/rfcs/0015-json-handler.md. Let's make this one the official RFC.
The rest of my comments are smaller and can be resolved in the same pass. Once these are addressed I'm happy to see this merged as Under Review and move on to reviewing the implementation.
RFC 0016 (model-checking#4727) is the spec under review for this feature and covers the same ground in more detail. Keeping both would leave two competing specifications for one flag, and would make the merge order between the two PRs significant. The design discussion belongs in model-checking#4727; this branch is the implementation. Note this leaves RFC number 0015 unused. model-checking#4727 numbered itself 0016 precisely to reserve 0015 for this PR, so that choice may be worth revisiting now that the file is gone.
|
@feliperodri thank you for detailed review of the PR. i admit i did not take all things into account, i wanted to start discussion in order to get faster feedback - which i got so thats great. i will fix all the comments, but just need one decision: should we aim for compelete version, i.e intial format to be stable but flexible (but strict per version)? meaning it comes with a schema and revision -- not a problem, just wondering. most of the added details are due to issues i experienced in rs-verified-der so for sure i may have missed some big things simply not having the need for them. so in short, how large coverage do you want from v1, model the full result or just a minimum as first revision. i dont mind which one but this raises another question - if we go for "full" format, could we do implementation in few PRs, simply due to possible size of work (if we go for all items included)? |
|
@feliperodri ok i will then split enhancemnts into issues with each PR (or you want one issue delivery covering them all with one PR?) |
Proposes an opt-in, -Z-gated --export-json <path> flag writing one machine-readable file per verification run: per-harness status, failed properties, check and cover outcomes bucketed exhaustively by status, the warnings CBMC tags as such, and the provenance needed to reproduce the run. Anchored on issue model-checking#942, which has requested exactly this document since 2022. The schema example is genuine output of the proof-of-concept implementation; the vacuity motivating case (a contradictory assume reporting SUCCESSFUL) is shown end to end.
Rename 0016-export-json.md to 0015-export-json.md and list it (plus the pre-existing 0014-harness-partition) in SUMMARY, so this supersedes the 0015 slot claimed by model-checking#4472's json-handler draft. Review fixes (overnight lens + model review, local only): - Remove the two <!-- Q2/Q3 --> review-scaffolding comments (belong in the PR thread, not the committed RFC; one re-asserted the [S] soundness label the maintainer ruled wrong). - Value domains: distinguish the covers buckets (satisfied/unsatisfiable) from the checks buckets (success/failure) in the exhaustive-partition prose, matching the example and the enum table. - Stabilization: attribute the open-questions-before-stabilization requirement to the RFC process/template, not RFC 0006 (0006's API Stabilization section does not state it; template.md does).
The "Fields that are not always strings" section called resolved_solver and resolved_unwind "always-string counterparts", but resolved_unwind is a number when present and null otherwise (as the worked example shows), and the global null rule permits null. State them as plain scalars (string/number/null, never objects) so a consumer following the prose does not break on the example's own resolved_unwind: null.
model-checking#4472 merged on 2026-08-12 and ships --export-json behind -Z unstable-options. The RFC no longer proposes a future flag. It now specifies the contract for a shipped one. - Summary and prior-work section state the as-built reality. - The -Z gate section states the shipped gate and proposes the dedicated ident as a migration step. - New first open question: does this schema supersede the shipped v1 shape, or is the RFC redrawn around it. - Drop the reference to closed PR model-checking#4732; run_state is proposed here, not implemented anywhere. - 'Do nothing' section names the second cost: the shipped shape becomes a de-facto unversioned contract.
Three corrections and a style pass, no change to the proposal itself. The directory-path bullet claimed argument-parse-time rejection as fact while the write-behaviour section described it as a follow-up. It now states the proposal and points at the note, so the two agree. Two loose uses of "sound" and "soundness" described reporting and consumer behaviour rather than a verdict, status or exit code. Reworded to say what they meant. Also dropped a reviewer request from the body, which belongs in the PR thread rather than the RFC. The prose carried 81 em-dashes across 670 lines where peer RFCs in the book use none, and leaned on "honest" as a stock adjective. Rewritten with sentence splits, commas, colons and parentheses. Code spans and fenced examples are byte-identical; the book still builds clean.
Two places described a path that is already a directory. The bullet under flag interactions said this RFC proposes rejecting it at argument-parse time; the write-behaviour note called that rejection a small follow-up, which put the same rule inside and outside the proposal at once. An earlier pass fixed the bullet and left the note, so the contradiction survived. The note now says adopting the RFC includes moving the failure to parse time, and that the shipped behaviour is an accident of implementation order. The completeness section pointed at "Q2". Open questions is an unnumbered list and never mentioned the completeness mechanism, so the reference resolved to nothing and the question the reference implied was open was not actually posed. Added it: marker versus delete-up-front, with why each fails in a different direction for a consumer that only checks whether the file exists, and pointed the sentence at it by name.
Address two gaps found by building the writer and by the downstream-consumer analysis: - `tools`: a single object with kani/rustc/cbmc/goto-cc/goto-instrument versions and a solvers[] list, restoring the machine-readable tool provenance the shipped model-checking#4472 shape carried (kani issue model-checking#2572) that this schema had dropped. - `selector`: the exact `--harness` string per harness (the module-qualified path), so an out-of-tree consumer has one stable, re-runnable key rather than reconstructing it from the file path. Also record, as open questions, the three shipped fields still dropped (workspace provenance, autoharness is_bounded/is_ctor_based, coverage.enabled) so their removal is a decision rather than a silent regression.
Addresses blockers and majors from the review at rev 2e6cf78: - Relabel the example as the proposed schema shown against a real PoC vacuity run, not a verbatim PoC dump (tools/run_state/warnings_truncated are proposed additions the PoC does not emit today). - Drop the redundant `selector` field: `name` (pretty_name) is already the module-qualified, --harness --exact-selectable string; document its crate-scoped uniqueness, the --exact requirement, is_automatically_generated and proof_for_contract edge cases, and why not mangled_name. - Add a normative field-reference table covering every field the example shows, including the failed_properties/unsupported_constructs/other[] element shapes, the checks/covers bucket-arithmetic invariant, and n_properties == checks.total + covers.total. - Fix the failure_kind soundness bug: it is the raw failure classification, not NONE iff verdict == SUCCESS (a passing should_panic harness is PANICS_ONLY). - Generalize the tools.* null rule, define solvers[] cardinality/ordering and its name-spelling agreement with resolved_solver, and add the conditional goto_synthesizer key. - Expand the dropped-fields open question into a full shipped-vs-proposed disposition (build_mode, mangled_name, end_line, goto_file, per-check detail, OS info, cbmc stats, is_ctor_based verified present in the shipped writer); flag is_bounded/effective object_bits as soundness- relevant and proposed mandatory; fix the coverage.enabled drop, which contradicted the RFC's own configuration policy. - Medium fixes: define file's base directory and connect it to the dropped workspace_root; fix the completeness-contract stale-file window wording; narrow the summary's reproduce-the-run claim; state the property-id format and ordinal-instability caveat; note the Serialize-derive gap on reused cbmc_output_parser types; add exact- minor pinning guidance for 0.x consumers. No design change: shape, vacuity predicates, and enum domains are untouched.
Source-verified against kani-driver's call_cbmc.rs, cbmc_output_parser.rs, frontend/schema_utils.rs, and kani_metadata for every claim below. Must-fix: - failure_kind: replace the wrong "disagree in exactly one case" / "NONE strictly stronger than SUCCESS" prose with the full should_panic truth table (call_cbmc.rs's verification_outcome_from_properties); scope the field to outcome.kind == COMPLETED, omitted otherwise. - Delete the stale top-level kani_version field-table row (absent from the example, superseded by tools.kani); reword tools.kani to stand alone and note it is the one tool version that is never null. - Add a presence matrix (marker vs terminal document; per-harness fields by outcome.kind) so "never absent/null" claims are correctly scoped instead of contradicted by INCOMPLETE markers and TIMEOUT/OOM/CRASHED harnesses. - Write the missing "Summary" section: define all 7 summary.* fields, the total == len(harnesses) and matched_count/total/run_state invariants, and how non-COMPLETED harnesses count (neither successful nor failed). - Define the 4 previously-undefined configuration.checks.* bools (memory_safety, overflow, unwinding, undefined_function). - Document the crate_name (rustc, underscored) vs cargo -p (Cargo package, often hyphenated) mismatch on the workspace re-run recipe. Field-mandatory decisions applied as ruled: - Promote harnesses[].is_bounded to a mandatory field (out of Open Questions), with a full definition of when it is true. - Promote configuration.coverage_enabled to a mandatory field. - Keep effective object_bits as an explicit open question, split cleanly from is_bounded, citing the model-checking#4731 triage. Should-fix: failed_properties[] membership + the n_failed == len(checks.failure) invariant + the class=="cover" partition criterion; a tools.solvers[].source ("builtin"|"probed") sibling to disambiguate an overloaded null version, plus the goto_synthesizer "requested" vs "ran" wording fix; the compatibility-policy minor-change clause scoped to explicitly-open vocabularies (solver names) instead of an empty referent, with attributes.kind/attributes.solver added to the value-domain table; a run-scoped CRASHED example message, a run_state x outcome.kind co-occurrence note, NO_HARNESSES_SELECTED vs a zero-harness crate, and a recommended symmetric covers-vacuity check. Nits: structural truncated/original_chars fields replacing the free-text truncation marker, a run-level OUT_OF_MEMORY scope note, and an under-listing fix in the example's framing paragraph. Validated: the main JSON example and the warnings example both json.loads() cleanly; mdbook build is error-free (pre-existing footnote warnings in other RFCs only).
Removes run-level outcome.kind == CRASHED: the shipped writer's single terminal write (verify_project in kani-driver/src/main.rs) happens once, after every harness result is known, so any hard error before that point (a kani-compiler crash, a driver panic, even a failed CBMC spawn for one harness) unwinds past it and no terminal document is ever written. A Kani self-crash therefore can only ever surface as a stale INCOMPLETE marker (or an untouched pre-existing file, if the crash predates the marker), never as a CRASHED value in a document that doesn't exist. Adds a run_state/outcome co-occurrence table covering every combination the schema can actually produce, and removes the now-dead run-level outcome.code/outcome.message fields (per-harness CRASHED, code, and message are unaffected and remain fully producible). Adds a forward-looking paragraph stating the schema is deliberately open to growing richer per-harness provenance/evidence over versions via the additive minor-version rule, rather than presenting v1 as a ceiling. Folds in several smaller consistency fixes: scopes the field-table "-" legend and the summary "always present" wording to terminal documents (both are absent in the INCOMPLETE marker); fixes the one remaining goto_synthesizer "ran" wording to say "requested", matching schema_utils.rs; fixes tools.solvers[].source to derive builtin/external from the actual resolution path (effective_solver's binary vs no-binary case) instead of the solver name, resolving the --sat-solver cadical vs --external-sat-solver cadical collision; states the schema_version-then- run_state consumer read order explicitly in both places it's implied; and merges the two NO_HARNESSES_SELECTED definitions into one.
Tighten the export-json RFC prose without touching normative content. Collapse multi-sentence explanations to single statements, delete restatements of field-table cells, cut over-hedging / meta-commentary, and trim rationale to its load-bearing core. Preserved verbatim: every table (field reference, value domains, presence matrices, run_state x outcome, failure_kind truth table), both JSON examples, and all code blocks. No field, type, nullability, predicate, guard, enum domain, compatibility rule, mandatory decision (is_bounded / coverage_enabled), open question, or out-of-scope item removed. 1302 -> 1035 lines; 100657 -> ~74KB; prose bytes 80554 -> 54909 (-32%). mdbook build clean; both JSON examples still parse.
Reorganize the document so the human-read body is ~half length, with the exhaustive machine-contract detail relocated (not deleted) to a new "Normative schema reference" appendix. Body keeps the design narrative: Summary/User Impact, the vacuity gap, the model-checking#4472 relationship, User Experience and flag interactions, the JSON example, the two vacuity predicates, the key decisions (is_bounded/coverage_enabled mandatory, name is the selector), the run_state completeness contract, and the Rationale, open questions, and out-of-scope sections. Appendix collects the field-reference tables, the presence matrices, the value-domain/enum table, the run_state x outcome co-occurrence table, and the failure_kind truth table, plus the per-field contract prose. All tables, JSON, and code blocks are byte-identical; no content removed. Also a light plain-English polish pass on relocated prose.
… + machine.*, defer to Future Per model-checking#4731 (feliperodri: 'cut v1 to the core'). Removes from the v1 schema: tools.goto_cc/goto_instrument/goto_synthesizer, tools.solvers[] ({name,version, source}), and the whole machine.* block (cpu_count/total_memory_bytes/memory_limit_bytes/ os/arch). Keeps tools.kani/rustc/cbmc and per-harness resolved_solver (the solver actually used stays recoverable). Both cut groups added to Out-of-scope/Future with a 'file a tracking issue on adoption' note. Updated: JSON example, field-reference table, Tool-provenance prose, presence matrix, value-domain table (solver spellings 3->2), compatibility-policy open-vocab list. Example JSON re-validated; mdbook builds clean.
|
@feliperodri misundestood, thoguht id have to discard this pr -- but now i understand this should stay PR for the RFC. changes requested from this review:
extra changes:
if anything of extra things should be removed, no problem. |
There was a problem hiding this comment.
🟡 Changes recommended
The schema omits result-altering flags and contains inconsistent stabilization and empty-selection requirements.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Defines an unstable JSON schema for machine-readable Kani verification results, including harness outcomes, vacuity signals, provenance, and completeness semantics.
Changes:
- Specifies the
--export-jsonschema and compatibility policy. - Documents result interpretation, failure modes, and future work.
- Adds RFC navigation entries.
File summaries
| File | Description |
|---|---|
rfc/src/SUMMARY.md |
Adds RFC 0014 and 0015 links. |
rfc/src/rfcs/0015-export-json.md |
Defines the structured verification-results RFC. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@ivmat can you address the Copilot comments? |
- configuration: add checks.assert_contracts and checks.prove_safety_only, the two shipped flags that weaken what a SUCCESS means without changing which properties exist; record the audit of VerificationArgs under the configuration policy (what is recorded where, what is excluded and why). - NO_HARNESSES_SELECTED: since model-checking#4743 a --harness filter that matches nothing is an error before the marker write and any export, so the state denotes only an unfiltered crate with no selectable harness; requested_filters is empty there and unmatched_filters is never the whole filter set. - Stabilization: drop the benchcomp migration prerequisite; its parser needs CBMC statistics this schema excludes by design, so it moves in the statistics follow-up, not before -Z is lifted.
Statement-by-statement audit against kani source found and fixed factual drift/overclaim across the document (flags semantics, schema-field sources, verification states, solver precedence, property-id forms, warning handling). No design change; corrections only. The 3 Copilot-comment fixes are retained.
|
@feliperodri this revision applies the requested changes and includes a source-accuracy pass, with statements re-verified against source. the rfc has been tightened with longer explanations in b part of the pr body |
Part A — Normative summary
RFC 0015 proposes a consumer contract for Kani's
--export-json <path>output. The flag shipped in #4472 behind-Z unstable-options. This RFC proposes a dedicated-Z export-jsongate and a versioned JSON document describing one verification run.The contract defines:
Shipped JSON already exposes processed property statuses and unreachable counts. This proposal specifies how consumers should interpret them, including when a successful harness has unreachable assertions and whether the export accounts for every selected harness.
The export complements
--sarif, which reports findings for code-scanning tools. The proposed JSON also includes successful-check counts and cover results.The schema remains unstable. Whether it should replace the shipped v1 shape, and how to resolve the remaining configuration and provenance gaps before stabilization, remain open questions. The RFC example illustrates the proposed schema; it is not captured verification output.
This PR changes documentation only. The RFC file contains the complete normative proposal.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.
Part B — Extended reference (informative — not part of the normative RFC)
Part B is informative and is not part of the normative RFC. It preserves the longer explanations and implementation notes moved out of the RFC and adds no normative requirements.
Read the extended reference
Extended reference (informative)
Supporting reference for RFC 0015 / PR #4727.
The RFC contains the proposed contract. This reference preserves explanations, implementation
comparisons and deferred-work detail moved out of it; it adds no normative requirements.
Interim home: a PR body comment. The explanatory material can later be adapted for the Kani book
or implementation documentation. Revision-specific observations below describe those revisions,
not a claim that one implementation already emits the entire proposed schema.
1. Revisions and illustrative example
Source descriptions below use checkout
902daa525. Historical PoC behavior is qualified by7b125f1b; later upstream behavior from #4717/#4719 is qualified by10bc1ae04. These revisions havedifferent capabilities; the proposed schema is not a description of any one of their emitted files.
The RFC output example is an example of the proposed 0015 document, for the vacuity case
the motivation describes: two ordinary assertions made unreachable by one contradictory
kani::assume. The PoC at7b125f1b47e36ca4cc50c4041abeca01912f80f9contains a synthetic unit test,export_checks_unreachable_under_contradictory_assume, that constructs two unreachable assertions anda successful result. It supports the illustrated result semantics, but is not a captured verification
run. The RFC's harness, provenance, timings, and JSON are illustrative, not a measured run or verbatim
PoC output. That PoC emits flat
kani_version/cbmc_versionfields (not the groupedtoolsobject) anda boolean
run_complete(notrun_state), and lacksis_boundedandconfiguration.coverage_enabled. It already hassummaryand all seven totals; this RFC refines theirtreatment of non-completed harnesses, as specified in the RFC.
The marker, warning caps and directory-path validation are proposed changes, not PoC capabilities.
At
902daa525,--fail-fastcollects aResult<Vec<_>>and returns only the failing result onerror, discarding other results even if those harnesses completed. The later #4744 fix retains
completed results. Completeness measures reported entries, not which missing harnesses ran.
2. Field explanations and warnings
Tool provenance (
tools). Thetoolsobject records the versions of the core tools whose behaviourcan change a result:
kaniitself (always known, so nevernull), therustctoolchainkani-compilerwas built against, and
cbmc. It is the machine-readable form of the versions Kani already prints,narrowing the gap in #2572. One rule for the two
probed entries: a version that cannot be determined is
null, never guessed — whether the probebinary is missing, refuses
--version, or prints nothing parseable. (An earlier draft also carriedgoto_cc/goto_instrument/goto_synthesizerversions and asolvers[]array of every resolvedsolver; those are cut from v1 to keep it to the core — see the "Full tool & solver provenance"
future-work item. The solver CBMC actually ran with is still recoverable per harness from
harnesses[].resolved_solver.)Harnesses that are not
--harness-selectable. Anis_automatically_generatedharness (fromkani autoharness) cannot be selected by--harness/--exactat all:find_proof_harnessesskipsgenerated harnesses regardless of filter. Its
nameis still reported (a real, unique identifier), buta consumer must not use it as a
--harnessargument whenis_automatically_generatedistrue. A#[kani::proof_for_contract]harness, by contrast, is selectable, and itsnameis the harnessfunction's own path — not the target it proves a contract for (that is
attributes.kind.ProofForContract.target_fn, see §7).Why not
mangled_name.HarnessMetadataalso carries amangled_name(the harness's name inCBMC's symbol table), but this schema does not export it and it would not serve as a selector: it
identifies the harness to CBMC, not to Kani's CLI, and
--harnessdoes not accept it.name(
pretty_name) is chosen because it is simultaneously human-readable, module-qualified, and directlyre-runnable.
checks/coversbucket every included property exhaustively by CBMC status. This partition andn_propertiescover exactly what they bucket;code_coverage(COVERED/UNCOVERED) properties under--coverageare outside both andn_properties(no dedicated place today), son_properties == checks.total + covers.totalholds on aCOMPLETEDharness.checks.successis a bare count, unlike every other bucket: successful-checkidentities are numerous and of little value, whereas
covers.satisfiednames its properties because coversare user-authored and few. Exporting successful-check identities is future work.
The
checks/coverspartition criterion. A property goes intocoversexactly whenproperty_id.class == "cover"(Property::is_cover_property,kani-driver/src/cbmc_output_parser.rs);every property whose class is neither
"cover"nor"code_coverage"goes intochecks(including"assertion", which carries ordinaryassert!/panic checks). This is a syntactic test on theCBMC-assigned class, independent of
status: a cover-class property carrying a status normally seenamong checks (or vice versa) still partitions by
classand is recorded in that domain'sother[]arrayrather than crossing over (see the RFC’s Value domains).
code_coverage-class properties are excluded from both buckets entirely.failed_properties[]membership.failed_properties[]names exactly the properties inchecks.failure(classis neither"cover"nor"code_coverage", withstatus == "FAILURE"), withthe full per-property shape rather than
checks.failure's bare id list. It does not includechecks.error(anERRORis solver-level "could not determine," bucketed separately — seefailure_kind == "ERROR") or any cover-class property(
covers.unsatisfiableis that bucket's own list). This is whyn_failed == len(checks.failure)on aCOMPLETEDharness:n_failedisfailed_properties.len(), whose membership ischecks.failure's.warningsis empty in the illustrative vacuity example. The synthetic fragment below shows theshape of an untruncated warning, not a captured CBMC message or a warning-size measurement. The
warnings_truncatedfield and thetruncated/original_charspair are this proposal's additions, notemitted by the PoC at
7b125f1b, which collects warning strings without truncation:{ "warnings": [ { "message": "Example warning", "truncated": false, "original_chars": null } ], "warnings_truncated": 0 }truncated/original_charsreplace a[truncated; N chars total]suffix insidemessage: astructural field a consumer checks without pattern-matching free text, avoiding the text parsing
this RFC argues against.
original_charsisnullwhentruncatedisfalse(nothing was cut). See “Statistics and truncation” below for what this field does and does not promise.
Statistics and truncation
Symex time, VCC counts and solver time exist only inside CBMC's free-text messages; extracting them requires
pattern-matching human-readable output, which causes the fragility this RFC is intended to prevent.
benchcompdoesexactly this today; the right fix is structured data from CBMC, not more scraping. The
warningsfieldcarries CBMC's messages as non-contractual free text, subject to the caps below: CBMC's
--json-uistream tags eachmessage with a
messageType(e.g."WARNING"), and this schema includes theWARNING-typed messages, forexample the SAT backend's
warning: ignoring forall/warning: ignoring exists, emitted when itcannot encode a quantifier with non-constant bounds. Later upstream
PR #4719, present at
10bc1ae04but absent at902daa525, explicitly parses these prefixes, counts the ignored quantifiers, and overrides theproperty-derived result with failure and
FailedProperties::Error. This internal interpretation doesnot make warning text a versioned interface: an export consumer treats each message as opaque display text and uses Kani's
computed outcome instead of pattern-matching it. Two consequences:
warningsis outside theschema_versioncompatibility guarantees. The field's presence and its[{ "message": …, "truncated": …, "original_chars": … }]shape are contractual; the contents ofmessageare not, and must not be pattern-matched or assumed stable across versions.warningsis bounded, with explicit, structural truncation markers. The PoC collects warningstrings without a length or count cap, so it places no fixed bound on their contribution to file
size. This proposal caps each
messageat a fixed, implementation-defined length and truncates iton a character boundary, with the fact and extent carried as siblings
truncated: boolandoriginal_chars: integer | null(pre-truncation count,nullexactly when
truncatedisfalse) rather than a suffix insidemessage. The per-harnesswarningsarray is separately capped at a fixed count, with a sibling
warnings_truncatedinteger per harness(
0when nothing was dropped) recording how many entries were omitted. A consumer can always tell"no more warnings" from "we stopped recording". These structural fields are schema-versioned; the
messagetext is not.Warning retention by outcome
Warnings do not depend on a property result array: CBMC may emit messages and then crash or be killed.
The PoC at
7b125f1bpreserves collected warnings on its no-results crash/OOM path. Its timeout pathinstead constructs a fresh result with no warnings. The export preserves whatever warnings the driver
retains, applying the same caps for every outcome;
[]means none were retained, not proof that CBMCemitted none.
3. Migration inventory
The inventory wording below is retained from the longer draft. Its shorthand for per-property detail
is inconsistent with that draft’s field tables: successful checks are counts, and
other[]holdsonly
{id, status}. The RFC retains those field-table definitions unchanged.--export-jsonfor structured verification results #4472'sdocument (
create_metadata_json,create_harness_metadata_json,process_cbmc_resultsinkani-driver/src/frontend/schema_utils.rs) carries a number of fields this schema does not restoreas-is. Grouped by proposed treatment:
Dropped, no successor field today:
build_mode("debug"/"release");harnesses[].mangled_name(see “Why not
mangled_name” in §2); the source range'send_line(onlyoriginal_start_linesurvives, as
harnesses[].line);goto_file(the generated modeling file path); per-checkdescription/locationfor every property, not just the failed andother-bucketed ones (theshipped shape records full detail for every check; this schema records identities only for
success/unreachable/undetermined/error/unknown, and full records only forfailed_properties/unsupported_constructs/other[]); CBMC's OS banner (cbmc_metadata.os_info);and CBMC execution statistics
(
cbmc_stats: symex time, VCC counts, solver time — see "Why is CBMC statistics data excluded?" andthe "Per-check timing" future-work item).
The effective
--object-bitsvalue stays an open question. It configures CBMC's pointer-objectencoding and is useful verification provenance. The shipped writer already records it per harness
at
cbmc[].configuration.object_bits, resolving either Kani's default or the explicit--cbmc-argsvalue. Its source is per-run state, so this schema could restore it underconfigurationor retain a per-harness field. On triage (--export-json: a failed, empty, or partial run can serialize as a clean pass #4731) a maintainer(feliperodri) classed it niche relative to
is_bounded. Leaving it dropped is a provenance gap:adopting this RFC should file (or update) a tracking issue for its representation, resolved before
or as part of stabilization.
The Cargo provenance
project.workspace_root/output_dir, andharnesses[].file's basedirectory.
harnesses[].fileis written relative to the invocation directory(
std::env::current_dir()), not any field this document carries; for acargo kanirun from theworkspace root the two coincide, but for a standalone
kani-driverinvocation or a script thatcdsfirst they need not. Restoring
workspace_root(or definingfileas workspace-relative) makesfileresolvable by a consumer that does not trust the launch directory.--target-dirdoes affectoutput_dir: Cargo builds derive it as<target-dir>/kani/<triple>/debug/deps(using Cargo's targetdirectory when the flag is absent). It can stay dropped as an artifact-location field, but cannot
be assumed to use the default path.
is_ctor_basedis absent fromHarnessMetadataand the shipped writer at902daa525, butpresent in both at later upstream
10bc1ae04. There it records constructor-based generation ormined-invariant filtering: success covers only values admitted by that mechanism. This is a
soundness-relevant restriction, independently of
is_bounded. Its representation remains open inthis draft, but support for exporting these harnesses must carry the restriction explicitly before
stabilization; omitting it cannot justify reading their results as unrestricted proofs.
4. Configuration explanations and implementation inventory
configuration.checks.assertion_reach_checks. Whether Kani inserted reachability checks ahead ofordinary assertions (
trueunless--no-assertion-reach-checkswas passed). With reach-checks off, anassertion made unreachable by a contradictory
kani::assumeis recorded inchecks.successinstead ofchecks.unreachable, silently preventing the vacuity indication this schema provides;configurationrecords exactly such toggles that change how a result must be read.
configuration.checks.ignore_global_asmand.extra_pointer_checks. Two more flags recorded forthe same reason.
ignore_global_asmmirrors--ignore-global-asm: whentrue, Kani did not error onglobal_asm!, so any behavior reachable only through that inline assembly is absent from the model anda proof can pass vacuously with respect to its effects.
extra_pointer_checksmirrors--extra-pointer-checks: whentrue, Kani adds obligations for invalid pointers in relationaloperations and pointer-arithmetic overflow, so two runs differing only in this flag check different
property sets, and
checks.total/checks.successare not comparable between them.configuration.checks.memory_safety,.overflow,.unwinding, and.undefined_function. Theremaining four
checksbools, each mirroring a--no-*-checksflag (all defaulttrue; the fieldname records the check being on, not the flag that turns it off):
memory_safetymirrors--no-memory-safety-checks(and--no-default-checks, the whole group).falseleaves out-of-bounds accesses and invalid-pointer dereferences unchecked; such a run can passwhile containing the memory-safety bugs Kani is intended to detect.
overflowmirrors--no-overflow-checks.falseomits CBMC's NaN check and disables itsdivision-by-zero check. It does not disable Rust/MIR arithmetic-overflow or integer
division-by-zero assertions: Kani still compiles with
-C overflow-checks=onand emits those MIRassertions (subject separately to
prove_safety_only).unwindingmirrors--no-unwinding-checks.falsestops CBMC asserting the loop/recursion boundcovered every execution, so a bounded proof can silently miss behavior past the bound.
undefined_functionmirrors--no-undefined-function-checks.falseskips Kani's generation ofassert-false-assume-falsebodies for undefined functions. Calls remain, with CBMC's defaultnondeterministic-return behavior; this does not model their possible side effects.
configuration.checks.assert_contractsand.prove_safety_only. These flags change generatedchecks and assumptions, and therefore what a successful result establishes:
assert_contractsmirrors--no-assert-contracts(trueunless it was passed; it requires-Z function-contracts). Whenfalse, ordinary calls to contracted functions useContractMode::Original: the original body executes and its contract instrumentation is omitted(
kani_middle/transform/contracts.rs). The contracts are neither asserted nor assumed, so successdoes not establish those contract obligations. Explicit
proof_for_contractchecking andstub_verifiedreplacement modes take precedence and retain their respective instrumentation.prove_safety_onlymirrors--prove-safety-only(defaultfalse; requires-Z unstable-options).When
true,codegen_assert_assumeemits only an assumption forPropertyClass::Assertion. Thisincludes ordinary user assertions and panic checks; success is conditional on those assumptions,
not evidence that the converted assertions hold. Direct
codegen_assertcalls remain checks,including the internal
CheckHook'sAssertion-class properties. The conversion is specific to thecode-generation path, not every property with that class.
All nine meet the same "changes which properties are generated, or what a status means" test.
Partial inventory of
VerificationArgs(at902daa525). Recorded directly: thenine
checksbools,coverage_enabled, andcbmc_args. Recorded elsewhere in the document, so notduplicated here:
-Zfeatures that add or remove properties (uninit-checks,function-contracts,loop-contracts,mem-predicates,quantifiers,stubbing) are visible by name inenabled_unstable_features;--unwind/--default-unwindin per-harnessresolved_unwind, resolved inthe order CLI
--unwind> harness#[kani::unwind]>--default-unwind(is_boundedinstead describesautoharness argument generation);
--solverin the per-harnessresolved_solver;--harness-timeoutinharness_timeout_sand theper-harness
outcome.kind. This inventory is incomplete:--synthesize-loop-contractstransformseach goto model before CBMC, while
-Z restrict-vtable --no-restrict-vtabledisables virtual-callrestriction without changing the recorded
-Zfeature set. Neither effective setting is representedin the proposed fields. Their representation and the rest of the inventory must be resolved under
the policy before stabilization;
cbmc_argsdoes not capture Kani-generated options.The proposed exclusion of
--randomize-layout [seed]is a scope choice: it changes the program undertest (type layout), so its seed belongs in a future subject/provenance block. Presentation and
execution options also need individual treatment: regular/terse output controls rendering, but
--output-format=olduses mock results and is rejected with export. At902daa525,--quietsuppresses the failure exit in
print_final_summary,so a failed harness can be exported as failed while Kani exits 0. That is a driver bug, not a
different verification verdict.
--jobschanges concurrency and can affect resource outcomes andwhich results exist under fail-fast;
--concrete-playbackchanges trace generation and formulaslicing as well as producing tests. These options cannot all be classified as affecting only presentation. Future flags
must be assessed under the same policy in the PR that adds them.
configuration.coverage_enabled. Mirrors--coverage(mandatory bool, defaultfalse). Itcauses
code_coverage(COVERED/UNCOVERED) properties to exist; these are excluded fromchecks,covers, andn_properties, so it meets theconfigurationpolicy, and without it a consumer cannottell "no coverage properties in this schema yet" from "
--coveragenever passed." It sits alongsidechecks.*, not nested under it.The configuration policy itself remains in the RFC.
5. Deferred provenance and memory work
kani/rustc/cbmc. An earlier draft of this schemarecorded
goto_cc/goto_instrument/goto_synthesizerversions and atools.solvers[]array of{name, version}per resolved solver (the shape the shipped Add--export-jsonfor structured verification results #4472 writer already emits), plus aproposed
source: "builtin"|"external"discriminator (this schema's own addition, not part of Add--export-jsonfor structured verification results #4472)to disambiguate a
nullversion. Cut from v1 to keep it to the core (permodel-checking/kani#4731): probing the extra
tools' and external solvers'
--versionspawns a process per tool beyond the probes v1 already needs,and the solver CBMC ran with is already recoverable per harness from
harnesses[].resolved_solver. Afollow-up can restore the fuller
tools.solvers[]block if a consumer needs the solver binaryversions. File a tracking issue on adoption.
machine). An earlier draft carriedmachine.cpu_count,.total_memory_bytes,.memory_limit_bytes,.os,.arch. Cut from v1: environment metadata thathelps triage performance/OOM but is not needed to reproduce a verdict. A follow-up can reintroduce a
machineblock if performance-oriented consumers ask for it. File a tracking issue on adoption.prover data), but not available from CBMC's structured output today.
--concrete-playbackalready derives a concrete valuevector; exposing it would let a consumer reproduce a failure without modifying source.
or reachable undefined functions, then collapses them; separating them would let a consumer tell
"raise
--unwindand retry" from "fix the code".vacuity concern.
kani-covand RFC 0011.stub_verifiedis sound only if the contract's ownproof passed. Kani already enforces at compile time that such a harness exists; because this schema
records both relationships, a consumer can check the run-time condition itself today, and Kani could report the
resolved status later.
getrusage(RUSAGE_CHILDREN)-based approach was prototyped and rejected:ru_maxrssis a process-wide running maximum, not a per-child figure, so the result isorder-dependent (only a harness whose peak memory exceeds that of every earlier harness gets a value; a later one with
a lower peak reads
nulleven under memory pressure) and is not attempted under--jobs, where the counter is sharedacross siblings. An accurate figure needs per-child accounting (e.g.
wait4()-based rusage per child,or a per-child cgroup with its own
memory.peak), which this schema does not attempt. Kani infers OOMfrom the CBMC child's status 137 when no property result array is available, and this schema
exposes that inference as
outcome.kind == "OUT_OF_MEMORY". Kani normally exits 1 for the failedharness, not 137; at
902daa525, the--quietbug can instead leave its exit code at 0.6. Failure and completeness explanations
Failure scenarios.
The verification verdict is computed independently and never rewritten by an export problem. The
up-front marker write fails before verification starts; the terminal export runs after the harness
verdicts but before the SARIF artifact and final summary line, so a terminal export failure aborts
those remaining steps. (Whether it should suppress the SARIF write is an implementation question, not
settled here.)
tools.*version cannot be determined → that field isnull, never guessed; see "Toolprovenance" above.
outcome.kind(
TIMEOUT/OUT_OF_MEMORY/CRASHED) records it and the file is still written. Run completeness isverdict-independent: as long as every selected harness has an entry in the terminal document,
run_stateisCOMPLETE(a suite where every harness times out is complete accounting for unsuccessful verificationattempts).
PARTIALmeans the terminal document lacks some selected harness results;INCOMPLETEmeans only the marker was published.
kani-compilerICE, a panic inkani-driver, aSIGKILL) → no terminaldocument is written: the single terminal write happens once at the very end (
verify_project), and anyhard error before it unwinds past it. There is no run-level
"CRASHED"value; all that remains is astale
INCOMPLETEmarker (or, if the crash preceded it, whatever file already existed). See "Howrun_stateandoutcome.kindco-occur" below.--export-jsonpath already holds a file → it is overwritten up front with an atomicrun_state: "INCOMPLETE"marker once harness verification begins (after the crate is built), so anearlier run's results cannot be misread as this run's, and a run that terminates after that without finishing leaves a file that
records that it did not finish (replacing an earlier delete-up-front design; see the RFC’s completeness
contract and open question).
create_dir_all), matching--sarif. A paththat is itself an existing directory fails today only when the write is attempted (the same-directory
temp write cannot be created inside a file-shaped target); adopting this RFC moves that to the
argument-parse-time rejection proposed under Interaction with other flags, so the failure occurs
before any verification work, as for
--sarif. The shipped difference is an accident ofimplementation order, not intentional.
How
run_stateandoutcome.kindco-occur.outcomeis meaningful only once Kani reaches aterminal write, and at run level its
kindhas exactly one value:COMPLETED. The marker(
run_state == "INCOMPLETE") is written before that and carries nooutcome. A run-leveloutcome.kind == "CRASHED"does not exist (see the crash scenario above); what a consumer observesinstead is a stale
INCOMPLETEmarker, or a missing file. The crash indication is that staleness ratherthan a field value. Every producible combination:
run_stateoutcomeINCOMPLETE(marker)COMPLETE{"kind": "COMPLETED"}PARTIAL{"kind": "COMPLETED"}NO_HARNESSES_SELECTED{"kind": "COMPLETED"}Per-harness
outcome.kind == "CRASHED"is unaffected and remains possible: the run can finish, reachits terminal write, and report that CBMC crashed on one harness. This describes one harness in a
completed document, distinct from Kani never reaching the write.
NO_HARNESSES_SELECTEDmeans exactly one thing: an unfiltered run of a selected project or workspacewith no selectable harness (no
#[kani::proof], or underautoharnessno eligible function). Itcannot arise from a
--harnessfilter: a filter set that matches nothing — and, with--exact, any single filterthat matches nothing — is rejected at harness selection (
no_harness_match_errorinkani-driver/src/metadata.rs, since #4743) with a non-zero exit. Harness selection precedes theINCOMPLETEmarker write and every export, so that case produces no document at all, not aterminal document with a non-empty
requested_filters. Hence in aNO_HARNESSES_SELECTEDdocumentharness_selection.requested_filters == [],unmatched_filters == [], andmatched_count == 0.unmatched_filtersis non-empty only in aCOMPLETE/PARTIALdocument written without--exact, whereat least one filter matched and the named ones did not.
Cover-only vacuity is not detected by this schema alone. The RFC’s normative and advisory
vacuity predicates read only
checks.*; anunreachablecover is recorded incovers.unreachableandsatisfies neither. A consumer wanting the corresponding indication for covers should additionally apply
covers.total > 0 && covers.unreachable.len() == covers.totalper harness, since this schema does not apply it for them.nullalways means not measured or not applicable, never a guess, and is always distinguishablefrom
0,false, and[].7. Casing and requested versus resolved values
Casing. The file mixes three conventions, and one rule explains all three: a value keeps the
serialization of the Rust type it comes from, and this schema does not fork types to re-case them.
kani_metadataandcbmc_output_parsertypes directly(
HarnessAttributes,AssignsContract,CheckStatus); re-casing would fork those types, recreate theduplication problem in #3541, or break existing
.kani-metadata.jsonconsumers. Those three already deriveSerialize; butProperty,PropertyId,and
SourceLocationderive onlyDeserialize(never serialized out), sofailed_properties[]/unsupported_constructs[]/checks.other[]/covers.other[]are purpose-builtexport structs rather than a direct embedding of
Property.outcome.kind,verdict,failure_kind,run_state, everystatus), mirroring the reused CBMC-status types (CheckStatus,FailedProperties); the new enums(
Outcome,verdict,run_state) use it too, so a consumer sees one value convention across the file.(Multi-word values like
NO_HARNESSES_SELECTEDneed an explicitSCREAMING_SNAKE_CASErename, sinceCheckStatus'sUPPERCASEcoincides with it only for single-word variants.)kani_metadataattribute enums below, which keeptheir own serde derivation untouched for the same no-forking reason.
Fields that are not always strings. Two reused
kani_metadataenums have non-unit variants and soserialize as objects, not strings:
harnesses[].attributes.kindis"Proof"or"Test", but{"ProofForContract": {"target_fn": "…"}}for a contract-proof harness.
harnesses[].attributes.solveris"Cadical","Z3", etc. for a named solver, but{"Binary": "…"}for a custom solver path.
These are the requested attributes, carried verbatim from
.kani-metadata.json; the resolvedcounterparts (
resolved_solver,resolved_unwind) are plain scalars (a string or number, ornull),never the object forms above.
Reconciling the two solver spellings.
attributes.solveris the requested value inCbmcSolver'sPascalCase (
"Cadical", or{"Binary": "<path>"}for a custom solver);resolved_solveris thelowercase built-in name or custom binary path CBMC actually ran with. Effective precedence is the last
solver-selecting
--cbmc-argsoverride > CLI--solver> harness attribute > default: Kani appendscbmc_argsafter its own solver flags. Bare--smt2leaves the choice to CBMC and yieldsnull.Compare
attributes.solveragainstresolved_solverfor "asked for" vs "ran".This proposal follows
kani listby recording tool version and schema version in separatefields; see the RFC’s Compatibility policy.