Repository navigation
Map2Check 9.0.0: AFL++ engine, smart seeds, engine alternation, sound TRUE verdicts - #73
GuilhermeBn198 wants to merge 87 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Seeds now live in <cwd>/<hash>.seeds beside the scratch directory, which every phase recreates. KLEE's vectors and the fuzzer's discoveries go to its afl/; the fuzzer phase reads it as -i. With --seed-exchange the KLEE share is 0.6 so the third phase gets real time. The cwd is restored between phases under --debug too (the nesting that broke the store path), and main removes the store after the last phase unless --debug. The single-vector exportFuzzerVectorAsKtest, which read a log that no longer existed, is gone; task 3 replaces it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
At the end of the AFL++ phase (no violation found), up to 64 queue entries are replayed through the witness binary inside the seed store's replay/ directory -- so a replay cannot overwrite a recorded violation -- and the typed nondet log of each becomes a .ktest in ktest/. The witness runtime writes that log only on a violation (to keep KLEE forks from clobbering it), so the replay opts in with MAP2CHECK_SEED_REPLAY. KLEE then runs with --seed-dir, extension/truncation allowed and --seed-time at a quarter of its budget. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oofs From the final review of the smart-seeds plumbing: - the fuzzer corpus is converted for KLEE only on the hybrid's first phase (a KLEE phase follows), and the replays stop at 5% of the budget; - the seed store is wiped at the start of a run and removed on every way out of main (scope guard), unless --debug keeps it; - a seed ends at the first nondet read the converter cannot express, instead of skipping it and shifting every later value (KLEE matches seeds by position); - the store path is quoted in the KLEE and afl-fuzz commands. Found while tightening the tests: a KLEE proof (SUCCEEDED) was followed by the third fuzzer phase printing UNKNOWN last; a proof now ends the hybrid like a violation does. Under --debug the cwd is restored between phases even without the exchange, so only the last phase's scratch is kept. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the search
In SV-COMP abort() is not an error; programs use it to discard a path,
inline as well as through assume_abort_if_not (seq-mthreaded/pals_*:
'if(!(i2)) {abort();}'). KLEE runs with --exit-on-error-type=Abort, so
the first path violating such an assumption halted the whole search with
exit 0 and the run answered TRUE for a program with a reachable bug.
Every wrong TRUE of the tacasv2 control arm in the R15 Cover-Error sample
had this shape (pals_* and float-benchs/sin_interpolated_index-1).
NonDetPass now rewrites the program's own abort() calls into
map2check_assume(0), the same pruning assume_abort_if_not already gets.
It runs before the runtime is linked, so the runtime's abort (a recorded
violation) is untouched, and TargetPass still records reach_error before
a following abort. Pre-existing (v15); unrelated to seeding, shipped with
this round. Integration section 29.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
KLEE exits 0 whenever its state queue empties, and the frontend read that as an exhaustive exploration. It is not one when KLEE concretized a symbolic input (a nondet double pinned to 0: float-benchs/sin_interpolated_index-1 answered TRUE) or killed states with its own errors (a VLA of symbolic size: loops/insertion_sort-1-2 under --slice answered TRUE). kleeDroppedPaths() generalizes the HaltTimer check to both; such a run is a timeout. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
kleeDroppedPaths read KLEE's 'partially completed paths', which counts the paths an assumption pruned as well: klee_assume(0) on an already-false path is a KLEE user error, and even klee_silent_exit counts as partial. No program with an assume_abort_if_not or an inline abort could be proved any more -- the section 29 safe program went from TRUE to UNKNOWN. nondet_assume (KLEE) now prunes with klee_silent_exit, which leaves no test behind, and a dropped path is read from what a killed state does leave: any *.err or *.early test, besides the HaltTimer and a concretized input. Section 29 now requires the safe program to be proved. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two more ways KLEE ends without having explored every path, from the review
of the dropped-paths check: a KLEE that crashed (solver, LLVM assertion, the
OOM killer) writes no "done" lines and no .err while the paths it did finish
may have written NONE; and near --max-memory it stops forking ("skipping
fork") or kills states ("over memory cap") and can still exit 0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every hybrid phase recreates the scratch directory and used to run sbt-slicer again. On eca-* the slicer timed out (0.2T) in phase 1 and again in phase 2, and the run was killed past its budget with no suite: the 4 ERROR of the R15 slice arm. The slice -- or the slicer's failure -- is now kept beside the scratch (<hash>.slice/), keyed by the input's content and every setting that shapes the slice, for the later phases of the same run. Two environment knobs for the R19 arms, defaults unchanged until measured: MAP2CHECK_SLICE_CLEANUP (none|light|o2, an opt pass over the fresh slice) and MAP2CHECK_SLICER_FLAGS (appended to sbt-slicer, e.g. --cda=ntscd). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the review: the MAP2CHECK_SLICE_CLEANUP opt pipeline ran unbounded, outside every engine's window. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On the eca-* modules (megabytes of IR) the three regex scans that collect the slicing criteria took ~45 s, outside every budget: with the slicer's own 0.2T that was 105 s before the first engine, and the run was killed past its deadline with no verdict (R19: the slice arm's 4 remaining ERROR). A plain scan with the same results (SlicerTest). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…self Past the end of the test case the AFL++ generator started over from its first byte, so 'while (__VERIFIER_nondet_int())' fed by the placeholder seed "A" never ended: afl-fuzz's dry run timed out and the fuzzer aborted before its first execution, on the loop idiom sv-benchmarks is full of. Reads past the end now return zero -- what the witness replay logs, so the suite built from that log stays exact. It also read data[0] of an empty test case. Covered by integration section 32 (next commit). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nates The fixed 0.2/0.6/0.2 split held an engine that had stopped progressing to the end of its share and cut one that still was. Under --alternate-engines (implies --seed-exchange) the fuzzer and KLEE take turns until a violation, a proof or too little time: each phase is bounded by a window that doubles every round of its engine (0.1T fuzzer, 0.3T KLEE) and ends when its engine goes max(10 s, 0.05T) without new coverage -- AFL_EXIT_ON_TIME for the fuzzer, a watch over KLEE's run.stats (SQLite, optional at build time) that SIGINTs it. A KLEE stopped that way is incomplete, never TRUE. Between turns: KLEE's latest tests seed its next turn (kleeprev/), the fuzzer's own seeds are not replayed back to KLEE, at most 64 KLEE vectors go to a short fuzzer window, and the test suite numbers after the cases an earlier phase wrote instead of overwriting them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Slicing through sbt-slicer rather than a linked dg, the AFL++/KLEE coordination inside the C++ frontend rather than a Python coordinator with IPC, the verdict fixes the TACAS rounds found, and what is still open (2.4 knobs and 3.2.4 in R19, 3c ranking, the MemoryTrackPass intrinsics). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The Cover-Branches cap and duplicates count across phases: every KLEE turn added up to 50 cases, mostly replays of the seeds it was given. The writer knows the cases already in the suite, and a run starts from an empty one (re-running in a directory kept the previous run's cases). - Budget: each phase measures its own setup (compile, instrument, link), which no window covers; a phase starts only if what is left covers it, and its window leaves it out. The fuzzer keeps KLEE's 5 s reserve. - The stagnation SIGINT goes to KLEE itself, not to timeout, which forwarded it to the child and its group: two signals, and the halt dump was lost. - KLEE's stagnation period doubles with its rounds, so it can still finish the paths that make a proof after coverage stops rising. - The fuzzer corpus in the store is capped (256), kleeprev/ is kept only when alternating, the store and slice cache are recorded for cleanup as soon as a phase starts, and the flag warns and stands down without --timeout or with --nondet-generator. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…growing patience R19 lost 5 eca-* tasks to --alternate-engines against the fixed hybrid. The fixed hybrid covers them in its last fuzzer phase: completed with zeros past their end, some of KLEE's partial paths reach the error in afl-fuzz's dry run (sig 06). The alternation sent only the 64 latest KLEE vectors and dropped exactly those. The cap (and the corpus cap) came from a nondet loop whose calibration never ended -- fixed since by the zeros past the end. Two more costs of each fuzzer turn on large programs: the three AFL++ binaries were rebuilt every phase (~24 s on eca-*; now cached per run by the modules' content, <hash>.build/), and the fuzzer's fixed 15 s stagnation cut ended its turns at ~17 s; its patience now doubles per round like KLEE's. Problem06_label05: UNKNOWN -> FAILED at 157 s (the fixed hybrid: 281 s). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fuzzer, witness and CmpLog builds were each bounded by 0.25T, so in sequence they could take 0.75T; on eca-* they ran the process past its deadline with no verdict (R19). They now share one deadline. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nters MemoryTrackPass matched the memory intrinsics by name, and the names carried the pointer types (llvm.memset.p0i8.i64) that LLVM 16 no longer writes (llvm.memset.p0.i64): no memset, memcpy or memmove clang emitted was checked, and memmove never was. memset(p, 0, 16) on 8 malloc'd bytes came back UNKNOWN. Dispatched by the intrinsic's kind now (MemSetInst, MemTransferInst), plus the libc names; memset's size is widened to the i64 map2check_load takes. In-bounds copies -- from string constants, struct assignment, memmove -- stay TRUE (integration section 34). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every missing AFL++ binary was reported as one that 'did not build within Ns' -- the budget's fault -- including a link error (an undefined reach_error) that no budget would fix. The build's status is kept: a timeout is still reported as one, anything else as a failure with the first line of afl-build.log that names the cause. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
KLEE takes at most 64 queue entries, and they were the oldest by id: once the queue outgrew the cap, what the fuzzer found late never reached KLEE. AFL++ marks the entries that hit new edges with ',+cov'; those now come first, each group in id order (tacas 3c, v1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
R26 complete. On SV-COMP the alternating hybrid, the new default, loses no correct TRUE: MemSafety correct-true is 10 vs 10, and wrong TRUE goes from 2 to 1. MemCleanup and NoOverflows are identical across arms. CASTLE matches v15 exactly (TP 54, FN 14, FP 1), and v15's 4 TIMEOUTs are now UNKNOWN. 9.0 campaign, first batches, paired with v15 on the same tasks:
Still running: Cover-Error (the rest), Juliet groups c and d, then Cover-Branches (2,765 tasks). |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cannot stop
Two defects the 9.0 campaign found on Juliet's CWE190/191 group:
- The AFL++ binaries were linked without -lm, so a program calling sqrt or
pow failed to link ('undefined reference to sqrt') and lost its fuzzer
phase -- 55 tasks the harness counted as ERROR. KLEE gets libm from
uClibc; the native links now pass -lm.
- A KLEE blocked inside a native external call (the fscanf variants hand
__isoc99_fscanf a uClibc FILE*) never checks its interrupt flag nor its
own --max-time, so the stagnation stop did not stop it: it held the rest
of its window until timeout's SIGKILL (207 of 272 stops in that group).
Not stopped within 10 s of the interrupt, it is killed and the time goes to
the next phase. Nothing is lost: a stagnated run is already incomplete.
Integration 48/50 under the campaign's load: sections 10 and 32 (fuzzer
timing) fail the same way on the previous build under that load, and pass on
rerun.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
The 9.0 campaign found two defects on Juliet's CWE190/191 group. Both are fixed in 76eedfc:
Neither defect changes a verdict; the second one only wasted time. The campaign continues on the fixed build, which is now this branch's head.
|
…plete Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
9.0 campaign: Cover-Error complete, 1,087 tasks paired with v15:
No category loses. The largest gains: ECA 58 → 146, ProductLines 111 → 169, Sequentialized 9 → 78. Juliet, complete groups:
Group c is being rerun on the fixed build. CASTLE is identical to v15. Cover-Branches has just started: 48.3% vs 42.1% on the first 111 of 2,765 tasks. |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sume-pruned KLEE paths Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts test klee_silent_exit kept a failed assumption from counting as a dropped path, but it also wrote no .ktest. A program whose every path meets a failed assumption got an empty Cover-Branches suite: in the 9.0 campaign 50 tasks (43 XCSP, 7 Floats) had tests on v15 and none on 9.0, costing 1.6 p.p. of mean coverage. _exit ends the path normally, so KLEE writes the test, and it skips the exit handlers, so an infeasible path reports no leak. Integration 50/50; busybox sleep-3.i memtrack stays UNKNOWN; xcsp Dubois-015/050 go from 0 tests to 100% validated coverage. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ed suite With e4e0b74 a path pruned by a failed assumption writes its .ktest. The suite is written phase by phase under a cap of 50, and on xcsp/AllInterval the first KLEE phase found only pruned paths: they filled the cap, the deep paths of the next phase found no room, and coverage fell from 95% to 9%. - NonDetGeneratorKlee.c makes a one-byte symbolic object named map2check_pruned before _exit, marking the .ktest. - readKtestFile strips the marker and reports it; readKtestVectors returns pruned vectors after all the others and flags them. - TestSuiteWriter marks a pruned case with an XML comment, remembers it across phases, and evictPrunedCase() removes one to make room. Pruned cases count toward neither the fuzzer's nor KLEE's share. AllInterval-011: 9% -> 100% (TestCov). Unit tests 12/12 with new cases; integration 50/50. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rs under BenchExec Test-Comp scores a task by the best coverage any of its validators measures (competition-scripts, export_adjusted_csvs.py, update_score_tester): TestCov with gcc and clang, formatted and unformatted, plus TestCoCa. Our harness ran one TestCov 3.7 with a 300 s limit and no BenchExec. tests/testcomp/official/ reproduces the testcomp26 setup: - Dockerfile: the competition's user image (Ubuntu 24.04), BenchExec 3.34, TestCov 3.11.post3 and TestCoCa from their Zenodo archives (checksummed); init.sh gives BenchExec its cgroup, as its container doc prescribes. - bench-defs/: the five official validator definitions, unchanged. - validate.py: narrows each definition to our tasks and suites, runs BenchExec from a fresh copy of the tool with the competition's overlay options, stages task inputs into a writable copy (TestCov writes beside the program), and merges the scores as the competition does. - validate.sh: runs it in the image, keeping /home free for BenchExec. run_testcomp_evaluation.sh keeps the suites in the layout the validators read when SUITES_DIR is set. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e runner testcomp.py and run.sh supersede validate.py and validate.sh: the same five official validators (TestCov gcc/clang, formatted and unformatted, and TestCoCa) now run under BenchExec from a reproduction image, with a non-official map2check.xml for generation. The BenchExec tool-info module takes the tool budget from cputime when the run set sets no wall-time limit, which is the Test-Comp case.
The competition machines have no AFL++, so make-archive.sh builds a 99 MB zip with LLVM 16, KLEE and AFL++, argv[0]-relative via a patchelf RUNPATH and free of symlinks. The wrapper points AFL++, AFL_PATH, AFL_CC and the passes at the bundled copies when they exist, and leaves the development-install defaults untouched otherwise.
eca-rers2018/Problem14.yml names its properties relative to the task (properties/unreach-call-a.prp), and stage_tasks copied only the .yml and its input files. BenchExec resolves every property a definition lists, so the one missing file made it drop the whole task set: all 140 suites of the TESTCOV_ERROR rerun came back MISSING. Copy a task's properties/ directory when it has one.
… disk BenchExec's per-run .files hold every test-suite, and 140 tasks reached 8 GB. Results are now compressed (BenchExec's default) and the output directory, the staged sv-benchmarks and the tool tree are removed after each validator; MAP2CHECK_KEEP_RESULTS=1 keeps them for debugging. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The official validators judge the suites kept in SUITES_DIR, so the harness no longer has to spend up to 90 s per task on TestCov 3.7. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
build_corpus_official.py reads the categories and excludes from the official bench-defs and samples each category in proportion to its size (build_corpus.py gives every category the same quota). Adds the whole applicable Cover-Error pool (1902 tasks) and a 3500-task Cover-Branches sample, plus the official ECA excludes set, without which ECA was over-represented. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…memory
NonDetPass instrumented sixteen __VERIFIER_nondet_* names. Any other one
stayed an external call KLEE could not resolve ("failed external call:
__VERIFIER_nondet_ulonglong"), which killed every path through it: in the V99
Cover-Error campaign all of Intel-TDX (0/658) and most of Floats came back
UNKNOWN with zero completed paths.
Each new type gets a KLEE and an AFL++ generator of its own width, a log row
that keeps its full value (64-bit and float rows no longer go through the
int-valued logger), and a name the ktest reader decodes. Floats are printed
with %.9g so they round-trip.
__VERIFIER_nondet_memory(p, n), used by the havoc_memory half of Intel-TDX,
is read by the validators' harness as n uchar inputs. KLEE makes one n-byte
object (a fresh one, copied in: klee_make_symbolic only takes the exact start
of an allocation), the reader expands it into one input per byte, and the
runtime logs it as n MEMORY_BYTE rows, where a seed converted from the log
stops.
_Bool, charp and uint128 stay unmodeled: the validators' harness has no
__VERIFIER_nondet__Bool or charp, and no task in the official corpora uses them.
Closes #74.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…categories Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Floats go from 11/33 to 33/33 in Cover-Error, the 15 regression guards stay FAILED, and all 48 suites score 1.0 on the three official validators. Intel-TDX stays at 0/12: the KLEE runtime writes symbolic nondet values with fprintf at exit and vfprintf forks per digit, which issue #75 tracks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…omp validation Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The runtime wrote klee_log.csv with the nondet values still symbolic, and uClibc's vfprintf forks on every digit it converts: with ten reads KLEE reached the target and never finished the log. Measured on a straight-line program with N reads and the target at the end: N before after 5 FAILED 28 s, 861 states FAILED 1 s 10 UNKNOWN FAILED 1 s 100 UNKNOWN FAILED 1 s 1,000 UNKNOWN FAILED 6 s 10,000 UNKNOWN FAILED 31 s - Pin each logged value to one model (klee_get_value + klee_assume, in order, so the row is one consistent vector) before printing it. - Format the rows by hand: even concrete, vfprintf cost ~7,000 KLEE instructions per call, 93% of a 1,000-read path. The file is byte for byte what fprintf printed (NonDetLogTest). - Past 1,024 rows skip the log: pinning is one solver query per row, and the violating .ktest, which the frontend already falls back to, holds the same vector. - Grow the realloc container geometrically instead of by one row, and drop the heap copy of every nondet row (append_element copies it; it leaked). Refs #75 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
KLEE's 2,000 MB default cap killed states on Intel-TDX tasks. The knob passes --max-memory; raising it to 6,000 MB left the 8-task probe at 0/8, so the default stays. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The full 9.0 campaign (the v15 corpora, paired task by task) is complete, and the assume-pruning defect it found is fixed and re-measured: see Full 9.0 campaign below.
Summary
This PR merges the whole TACAS line into one branch and sets the version to Map2Check 9.0.0. It covers:
--add-invariants.Nine stacked branches are merged, plus the release and default commits on top of
develop:feat/tacas-smart-seedsfeat/tacas-2d-slicingfeat/tacas-3b-alternation--alternate-engines, AFL++ build cache, fixes from reviewfeat/tacas-memtrack-intrinsicsfeat/tacas-3c-seed-rankingfeat/tacas-memtrack-strings%s/putsstring checks (off by default)feat/tacas-fuzzer-suitefeat/tacas-klee-vector-replayfeat/tacas-invariantsMAP2CHECK_PREOPT=ssaNew defaults (measured in R26)
--alternate-engines, with seed exchange) whenever--timeoutis given.--fixed-hybridkeeps the 8.x schedule.MAP2CHECK_FUZZER_SUITE=0drops it).--add-invariants(optional and under study, seedocs/backlog.md);MAP2CHECK_PREOPT=ssa;MAP2CHECK_CHECK_CSTRINGS;Why 9.0.0 and not 8.2
--nondet-generator fuzzeris now--nondet-generator afl.abort()prunes the path instead of ending KLEE's search.Changes
Verdict soundness (these fixes affect every mode)
abort()prunes the path.NonDetPassrewrites the program'sabort()intomap2check_assume(0). SV-COMP benchmarks useabort()as an assumption, both inline and throughassume_abort_if_not. With--exit-on-error-type=Abort, KLEE used to end its whole search on the first such path and answer TRUE. This was the cause of every wrong TRUE of the control arm in R15: thepals_*family andsin_interpolated_index-1.doublepinned to 0);*.error*.earlystate;donelines, meaning it crashed.klee_silent_exit. A failedklee_assume(0)is a KLEE user error, and it counted as a dropped path. An intermediate version of this PR had that bug: it readpartially completed pathsand could not prove any program that contains an assumption. Fixed and covered by a test.Engines and seeds
<hash>.seeds/(3a). The store lives beside the scratch directory, so it survives the phases. Exchange works in both directions:.ktestseeds for KLEE;--alternate-engines(3b). AFL++ and KLEE take turns:AFL_EXIT_ON_TIMEfor AFL++, and for KLEE the covered instructions read fromrun.stats(SQLite is optional at build time; SIGINT goes to the KLEE pid);<hash>.build/), and the three builds share one budget.+covare sent to KLEE first.Slicing
<hash>.slice/, keyed by content). A slicer failure is cached too.std::regex: on the eca-* programs the regex took about 45 s.MAP2CHECK_SLICE_CLEANUP=light|o2andMAP2CHECK_SLICER_FLAGS. None was promoted: every variant stayed within ±3 tasks.Memory safety
MemoryTrackPassmatchedllvm.memset/memcpyby their LLVM 6 names, so under LLVM 16 no intrinsic was checked andmemmovenever was. It now dispatches onMemSetInst/MemTransferInst.MAP2CHECK_CHECK_CSTRINGS=1, off by default. Under KLEE,printfruns as an external call, so a%sread of an unterminated buffer was never checked.cpybad.Test suites
MAP2CHECK_FUZZER_SUITE=1, off by default: the fuzzer corpus becomes Cover-Branches test cases, up to half of the suite.--add-invariants(INV-1 in the log)--crab-promote-assume. That flag emitsllvm.assume, whichNonDetPassdoes not map and which KLEE ignores. I tested this on the release's KLEE 2.1 and on KLEE 3.1, with and without--optimize. In those builds the invariants had no effect.--crab-track=arr --crab-add-invariants=after-load, and the invariants did reach KLEE.MAP2CHECK_CLAM_PROFILE=default|memory|none;MAP2CHECK_PREOPT=ssa(mem2reg + simplifycfg), for reachability and assert only. With it on, the memory modes produced FALSE-FREE on safe programs.Unmodeled nondet types (#74)
NonDetPassinstrumented only 16__VERIFIER_nondet_*types. A call tofloat,longlong,ulonglong,u8/u16/u32or__VERIFIER_nondet_memorywas an external call: KLEE killed every state and the task was UNKNOWN by construction.intlogger, and floats print as%.9gso they round-trip;nondet_memorygives oneucharinput per byte, which is how the validators' harness reads it.Result: TRUEon TestCov 3.11.KLEE nondet-log explosion (#75)
Cause. At the end of each violating path, the KLEE runtime wrote
klee_log.csvwithfprintf, and the values were still symbolic. uClibc'svfprintfforks on every digit it converts. KLEE reached the target, then multiplied states while writing the log and never finished.The fix, each part driven by
run.istats:klee_get_value+klee_assume, in order, so the row stays one consistent vector);printf. Even with concrete values,vfprintfwas 93% of a 1,000-read path. The file is byte for byte the same (NonDetLogTest);.ktestcarries the same vector, and the frontend already falls back to it;Measured on a straight-line program with N nondet reads and the target at the end, KLEE only:
Intel-TDX is still 0/8 on the 300 s probe, with KLEE's default cap and with
MAP2CHECK_KLEE_MAX_MEMORY=6000(new knob, default unchanged). The log no longer blocks KLEE: it now completes about 850 paths per task, where it completed 0 before. What remains is the programs' own path explosion: thousands of havoc reads followed by a long validity precondition.Tests: integration 51/51; section 38 checks KLEE alone behind 12 nondet reads.
Official Test-Comp 2026 validation
tests/testcomp/official/run.sh generate|validateruns the competition's own validators under BenchExec: TestCov gcc/clang unformatted and TestCoCa clang formatted. Each task gets its own property files, and each validator's scratch is dropped so a full corpus fits on disk.build_corpus_official.py), drawn from the Test-Comp 2026 task set with proportional stratification:Smaller fixes
while (nondet())loop never ended and afl-fuzz aborted in its dry run.Results
Full 9.0 campaign, paired with v15 (2026-10-05)
Setup. Both versions ran on the same tasks under the same conditions: the corpora of the v15 campaign, 300 s and 4 GB per task, compared task by task. 9.0 ran with its defaults: alternating hybrid, seed exchange, and the fuzzer corpus in Cover-Branches suites. The campaign build is
76eedfc75. The assume fix (e4e0b74c7,6848d2395) came after the campaign; its rerun is reported separately below.At a glance
Cover-Error: finding the error and proving its absence
A task counts only when TestCov runs the suite and confirms the error is reached. A wrong TRUE means the tool claimed the error unreachable on a task where it is reachable. The competition penalises that heavily, and it is what this line set out to remove.
By category (covered): ECA 58 → 146, ProductLines 111 → 169, Sequentialized 9 → 78, XCSP 22 → 38, Arrays 84 → 92, Loops 102 → 109. No category loses.
Cover-Branches: how much of the program the suite exercises
Score: the branch coverage TestCov measures when it runs the suite. A task with no validated suite counts as 0%.
By category (mean coverage, v15 → 9.0): Arrays 60.7 → 76.2, BitVectors 60.6 → 70.0, ControlFlow 44.9 → 53.1, Recursive 57.8 → 65.3, Heap 40.6 → 47.3, Loops 70.5 → 73.8, LinkedLists 75.8 → 78.5, Sequentialized 56.0 → 58.7, Floats 5.4 → 6.7, ProductLines 20.3 → 20.7, ECA 10.5 → 10.6. XCSP was the only category that lost, and the fix below recovers it.
The assume fix. In the campaign build, a path that a failed assumption pruned under KLEE wrote no test. 50 tasks (43 XCSP, 7 Floats) ended with an empty suite. The fix has two parts:
_exit), so KLEE writes its test;Rerun with the fix (mean coverage, same conditions):
The projected ≈47.0% replaces these 226 tasks in the campaign result; the other tasks were not rerun.
Re-run of the categories #74 affected (official corpus, 2026-10-12)
Same conditions as the V99 campaign on the official corpus: 900 s, 8 GB, 4 cores. All suites were validated by the three official validators.
Report:
docs/reports/2026-10-12-issue74-rerodada.md.Juliet C and CASTLE: memory-safety and overflow bugs
Each Juliet test comes in a bad variant, which has the bug, and a good variant, which has it fixed. Finding the bug in a bad variant is a TP. Saying it is safe is an FN. Reporting a bug in a good variant is an FP.
CASTLE is unchanged: TP 54, TN 44, FN 14, FP 1 on the 119 cases in scope.
Caveats
docs/backlog.md).docs/reports/tacas-experiment-log.md.Development rounds (samples)
All rounds use the Test-Comp samples
r15-ce.tsv(Cover-Error, 213 tasks) andr15-cb.tsv(Cover-Branches, 120 tasks), which are nested inside the v15 campaign, with 300 s per task. The per-round data is indocs/reports/tacas-experiment-log.md.v15 baseline (full campaign, before this line)
Cover-Error, 213-task sample
developafter #68–#71--seed-exchange--alternate-engines, before its fix--sliceand its 4 variants--seed-exchange, this branch--alternate-engines(the new default), this branchCover-Branches, 120-task sample
developR19 ran under heavier machine load than R15: 11 containers at once, and memory ran out at the end. That is why the R19 control scored lower on the same sample.
--add-invariantsand SSA (R25, 71 Cover-Error + 50 MemSafety tasks)MAP2CHECK_PREOPT=ssa--add-invariants" section above.SV-COMP (R17 and R21, 120 s per task)
errorresults in R17 came from the harness classifier, not from Map2Check (fixed).%scheck (R21) fixes one wrong TRUE and introduces one wrong FALSE. On CASTLE it raised FP from 1 to 4 (ascanfbuffer,argv[0]). It is off by default.%scheck on): TP 54, TN 44, FN 12, FP 4. Baseline R14: TP 53, FN 14, FP 1.Tests
ctest). New:AlternationTest, plus new cases inKtestReaderTest,SlicerTest,SeedStoreTestandTestSuiteTest.tests/integration/test_testcomp_regressions.sh): 50/50 with the new defaults; section 11 now checks the 8.x behaviour through--fixed-hybrid. Also 50/50 withMAP2CHECK_PREOPT=ssa. New sections 29–37 cover the abort rewrite, dropped paths, the slice cache, the AFL++ nondet loop, alternation, intrinsics,%s, the fuzzer suite and the link error.tests/integration/test_verdict_classifier.sh).Documentation
docs/backlog.md:--add-invariantsstudy;docs/reports/2026-08-16-crabllvm-review.md.docs/reports/tacas-experiment-log.md: every round, R1–R26, plus the official-environment rounds and NonDetPass: tipos __VERIFIER_nondet_* não modelados zeram tarefas (Intel-TDX, Floats) para UNKNOWN #74.docs/reports/2026-10-06-ambiente-testcomp-oficial.mdanddocs/reports/2026-10-12-issue74-rerodada.md.🤖 Generated with Claude Code