Skip to content

Harden check against verdicts the run did not establish - #43

Merged
jserv merged 3 commits into
mainfrom
dev
Oct 9, 2026
Merged

jserv merged 3 commits into
mainfrom
dev

Conversation

@jserv

@jserv jserv commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

This applies what seven external Frama-C projects (acsl-skills, dawnr, Deadlock_and_Racer, FermatVerification-bench, fragma, seal, VerNFR) taught about ways check could report "proved" for something the run never established, and closes each one that reproduced here.

The two that changed real verdicts: Frama-C 33 defaults -rte-use-eva-results on, so running EVA before WP removed runtime-error goals from every function EVA analyzed (an out-of-bounds write proved 5/5; it is 5/6 now), and a scoped check started EVA from the globals' initializers instead of an unknown state. Fixing the first surfaced a real overflow in tests/fixtures/test_abs.c that EVA had been hiding, and its contract is corrected. The plug-in now invalidates WP's cache after inserting ghost code, which before let run_wp prove the body as it was before the insertion. New incomplete codes and two informational fields say what a proof rested on; docs/design/verdict-hardening/architecture.md has the design, including one rule a Codex review found unsound and that was replaced.

Verified with scripts/run-gates.sh: all 18 gates pass, unit 705/705 and the MCP stdio suite 175/175, against Frama-C 33.0 with Alt-Ergo 2.6.3. Each soundness fix has a test that was confirmed to fail without it. The two intermediate commits build and pass their own tests on their own.

Left out, with their reasons in the design docs: a verdict narrowed by target (established reports the split instead), cross-architecture machdep generation, and a prover memory cap, which needs measuring first.


Summary by cubic

Hardens check so a "proved" verdict can no longer claim more than the run established, and repairs the two adjacent holes that allowed it: the concurrency screen and ghost insertion.

Verdict soundness

  • Frama-C 33 defaults -rte-use-eva-results on; every run now starts with -rte-no-use-eva-results, so EVA can no longer delete WP's runtime-error goals (an out-of-bounds write proved 5/5 before, 5/6 now).
  • That change surfaced a real overflow in tests/fixtures/test_abs.c, whose contract is corrected.
  • A scoped check now runs EVA under -lib-entry from an unknown global state instead of the globals' initializers, and run_wp puts -main and -lib-entry back, failing closed if the reset discards EVA's results.
  • Ghost insertion now invalidates WP's cache, resolves names through the enclosing blocks, and declares ghost locals in their own block; before, an inserted assignment's assertion could be proved against the unmodified body. Every mutating request runs the kernel's Filecheck.
  • check reports the recursive logic definitions WP assumes without a termination check and splits established verdicts by target.
  • New gap codes cover unencoded SV-COMP reachability properties, thread bodies never analyzed, compile flags dropped on database import, missing or unproved pinned properties, decorative contracts and unchecked contract mutants.
  • New opt-in load options rte_pointer and builtin_models, and a WP step budget, enter the proof receipt only when set; sandboxes inherit the project's RTE switches.
  • One-shot Frama-C runs now run in their own process group: they null stdin, wait on the exit, then kill the group, and abort their pipe readers on every path out, so orphaned provers cannot survive a timeout, a finished run is not held open by a pipe, and a failed or truncated read is an error rather than a short buffer.
  • Review fixes: pins unwrap \old only where the printer adds it, pinned admits a stringified array, compile-database flags are split as a shell splits words, and analyze_concurrency is suggested only after both sequential analyses ran.

Concurrency screening

  • A spawn now repeats when its creating function may run more than once (multiple call sites, loops, recursion, repeating callers), so a counter two thread instances increment raises a candidate.
  • __atomic_*, __sync_* and C11 atomic accesses no longer pair, SV-COMP atomic sections hold a pseudo-lock, only stdatomic.h type names count as atomic, and a failed compare-exchange's expected argument is a write.
  • Lock calls come from a table covering mutexes, spin locks, rwlocks, semaphores and cond waits; locks follow calls, so a two-wrapper ABBA is a candidate, and a lock reached through a parameter counts at each call site.
  • Lock order reports cycles as deadlock candidates (a reader-only cycle is not one), double locks, threads exiting holding a lock, and a continue inside a switch lands at the nearest loop instead of merging paths after the switch.

Each soundness fix has a test confirmed to fail without it; all gates pass against Frama-C 33.0 with Alt-Ergo 2.6.3. Left out with reasons in the design docs: verdicts narrowed by target, cross-architecture machdep generation, and a prover memory cap.

Written for commit 0dc5973. Summary will update on new commits.

View guided diff Turn on auto-fix

cubic-dev-ai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

jserv added 3 commits October 8, 2026 14:35
Inserting a ghost declaration, assignment, else-branch assignment or
label rebuilt the CFG without Ast.mark_as_changed, so WP's per-function
cache, which depends on Ast.self, kept proving the body as it was before
the insertion. Every body edit now ends in commit_body_edit, which
rebuilds and invalidates together. Ghost locals go into the blocals of
the block holding them, with vdefined set, which Filecheck requires.

Names in ghost expressions resolve through the enclosing blocks, then
formals, then globals, instead of any local of the function. Mutating
requests run Filecheck when AST_UTILS_CHECK_AST is set, and the
regressions load the plug-in this tree built, with the check on.

getRecursiveLogic lists recursive logic definitions with their
components, because WP assumes each one without checking that it
terminates. execSetWpConfig takes a step budget, and the ghost
registries, written and never read, are gone.
A thread created by a helper called twice, or through a wrapper taking
its start routine as a parameter, counted as running once, so a counter
both instances increment raised no race candidate. Spawns now repeat
whenever the creating function may run more than once.

Atomic builtins and the C11 stdatomic types are atomic accesses, and a
pair is dropped only when both sides are; compare-exchange writes its
expected argument. Lock calls come from a table covering mutexes, spin
locks, rwlocks, semaphores and condition waits.

Lock order lists each pair once with capped sites, reports cycles
between threads that may run together as deadlock candidates unless
every acquisition is a read with no writer anywhere, and reports double
locks and threads exiting with a lock held. Locks follow calls through
per-function summaries and entry locksets, so a two-wrapper ABBA is a
candidate. Measured at 0.68 s on a 39,005 line file.
Running EVA before WP removed runtime-error goals: Frama-C 33 defaults
-rte-use-eva-results on, which emits no annotation on a function EVA
analyzed, so an out-of-bounds write proved 5/5. Every Frama-C now runs
with -rte-no-use-eva-results (5/6). A scoped EVA run starts under
-lib-entry, run_wp resets -main and -lib-entry under main_eva_lock and
withdraws eva_completed when that discards EVA, and an aborted EVA run
is EVA_NOT_RUN. Fixing the first exposed a real overflow in test_abs.c.

New gaps say what a proof rested on: an SV-COMP property the harness
preconditions do not state, a threaded program, compile flags a
database loses, pinned properties missing or unproved, and a contract a
stub body also proves. check reports the recursive logic WP assumes and
the verdict split by target. rte_pointer, builtin_models and a WP step
budget are opt-in and enter the receipt only when set; sandboxes
inherit the project's RTE switches.

One-shot Frama-C runs get a null stdin, wait on the exit rather than
the pipes, and kill their whole process group however they end.
@jserv
jserv merged commit dc43a01 into main Oct 9, 2026
17 checks passed
@jserv
jserv deleted the dev branch October 9, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant