Repository navigation
Map2Check 9.0.0: AFL++ engine, smart seeds, engine alternation, sound TRUE verdicts - #73
Conversation
tacasv2a: --slice that keeps the suite valid (reach + assert)
tacasv2b: --slice for memtrack/memcleanup, and two correctness fixes
tacasv2c: --slice for overflow
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>
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 TestCov gate is bidirectional and failed on b4bda90 and a2fbc56 with "expected NOT_COVERED, observed COVERED". Concretizing the KLEE nondet log (8da5794) removed the state explosion that kept this four-read program from emitting a vector. Measured at BUDGET=120: install_v99b NOT_COVERED, install_v99c COVERED (2/2). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved instrumentation and output-consumer defects can break analysis or replay, with additional correctness issues in packaging and crash classification.
6 open issues
Handle new nondet CSV type IDs across parsing and witness generation · New Avoid renaming assumptions to an occupied runtime function name · New Recover violating .ktest data when klee_log.csv is absent · New Do not advance printf argument index for %m conversion · New Detect outer crashes before suppressing replay crash messages · New Resolve relative output paths before staging and archiving · New
What changed in this PR
This PR prepares Map2Check 9.0, combining AFL++/KLEE engine coordination, safer verdict handling, expanded nondeterministic input support, and competition validation infrastructure.
Changes:
- Adds persistent seed exchange, engine alternation, slice caching, and cross-phase test-suite management.
- Expands nondet and memory instrumentation while reducing KLEE logging overhead.
- Adds Test-Comp packaging, evaluation support, regression tests, and release documentation.
| File | Description |
|---|---|
| utils/testcomp-archive/make-archive.sh | Packages Map2Check and its engine dependencies. |
| utils/moduleBenchExec/map2check_testcomp.py | Uses CPU limits when no wall-time budget exists. |
| utils/moduleBenchExec/__init__.py | Adds the BenchExec module package marker. |
| utils/map2check-testcomp-wrapper.py | Configures bundled AFL++ tools. |
| tests/unit/frontend/TestSuiteTest.cpp | Tests cross-phase suite persistence and eviction. |
| tests/unit/frontend/SlicerTest.cpp | Tests nondet criteria and slice-cache helpers. |
| tests/unit/frontend/SeedStoreTest.cpp | Tests seed filtering and coverage ranking. |
| tests/unit/frontend/CMakeLists.txt | Registers seed-store and alternation tests. |
| tests/unit/frontend/AlternationTest.cpp | Tests engine windows and stagnation tracking. |
| tests/unit/backend-library/NonDetLogTest.cpp | Checks nondet CSV formatting. |
| tests/unit/backend-library/CMakeLists.txt | Registers nondet-log tests. |
| tests/testcomp/run_testcomp_evaluation.sh | Preserves suites and isolates manifest descriptors. |
| tests/testcomp/official/run.sh | Launches containerized generation and validation. |
| tests/testcomp/official/init.sh | Initializes BenchExec cgroups. |
| tests/testcomp/official/Dockerfile | Defines the competition validation image. |
| tests/testcomp/official/bench-defs/testcov-validate-test-suites-gcc-unformatted.xml | Defines unformatted GCC validation. |
| tests/testcomp/official/bench-defs/testcov-validate-test-suites-gcc-formatted.xml | Defines formatted GCC validation. |
| tests/testcomp/official/bench-defs/testcov-validate-test-suites-clang-unformatted.xml | Defines unformatted Clang validation. |
| tests/testcomp/official/bench-defs/testcoca-validate-test-suites-clang-formatted.xml | Defines formatted TestCoCa validation. |
| tests/testcomp/official/bench-defs/map2check.xml | Defines competition-style generation limits. |
| tests/testcomp/expected.txt | Updates the loop-input coverage expectation. |
| tests/testcomp/build_corpus_official.py | Builds proportionally stratified official corpora. |
| tests/memsafety/run_memsafety_evaluation.sh | Prevents child access to the manifest descriptor. |
| tests/lib/verdict_classifier.sh | Separates replay crashes from tool failures. |
| tests/integration/test_verdict_classifier.sh | Adds replay-crash classification coverage. |
| README.md | Documents engines, verdicts, and experimental options. |
| modules/frontend/utils/slicer.hpp | Adds faster criteria scanning and cache helpers. |
| modules/frontend/utils/seed_store.hpp | Defines persistent seed paths and ranking. |
| modules/frontend/utils/gen_crypto_hash.cpp | Rejects unreadable input files early. |
| modules/frontend/utils/alternation.hpp | Defines scheduling and stagnation helpers. |
| modules/frontend/test_suite/test_suite.hpp | Extends cross-phase suite-management APIs. |
| modules/frontend/test_suite/test_suite.cpp | Preserves cases and tracks pruned paths. |
| modules/frontend/test_suite/ktest_reader.hpp | Extends decoding and exploration-status interfaces. |
| modules/frontend/CMakeLists.txt | Adds threading and optional SQLite support. |
| modules/frontend/caller.hpp | Declares caching, seed exchange, and engine controls. |
| modules/backend/pass/NonDetPass.hpp | Declares additional nondet instrumentation types. |
| modules/backend/pass/NonDetPass.cpp | Rewrites abort calls and instruments new types. |
| modules/backend/pass/NonDetFunctions.hpp | Declares typed logging functions. |
| modules/backend/pass/MemoryTrackPass.hpp | Declares string-read instrumentation. |
| modules/backend/pass/MemoryTrackPass.cpp | Recognizes memory intrinsics and optional string checks. |
| modules/backend/library/lib/WitnessGeneration.c | Enables nondet logs during seed replay. |
| modules/backend/library/lib/NonDetLog.c | Optimizes formatting and supports wider values. |
| modules/backend/library/lib/NonDetGeneratorNone.c | Adds constant implementations of new nondet types. |
| modules/backend/library/lib/NonDetGeneratorKlee.c | Adds types, bounded logging, and pruned-path markers. |
| modules/backend/library/lib/NonDetGeneratorAFL.c | Adds types and zero-fills exhausted inputs. |
| modules/backend/library/lib/ContainerRealloc.c | Grows log containers geometrically. |
| modules/backend/library/lib/AnalysisModeMemtrack.c | Checks string reads against tracked allocations. |
| modules/backend/library/lib/AnalysisModeMemcleanup.c | Adds the string-check stub. |
| modules/backend/library/header/NonDetLog.h | Declares new typed logging APIs. |
| modules/backend/library/header/NonDetGenerator.h | Declares new nondet generators. |
| modules/backend/library/header/Map2CheckTypes.h | Adds nondet tags and 64-bit value storage. |
| docs/superpowers/specs/2026-09-29-tacas-3c-seed-ranking-design.md | Describes coverage-based seed ranking. |
| docs/superpowers/specs/2026-09-29-tacas-3b-engine-alternation-design.md | Describes engine scheduling and safeguards. |
| docs/superpowers/specs/2026-09-29-tacas-2d-slicing-optimizations-design.md | Describes slice caching and experiments. |
| docs/superpowers/specs/2026-09-28-tacasv3a-smart-seeds-plumbing-design.md | Describes bidirectional seed exchange. |
| docs/reports/2026-10-12-issue74-rerodada.md | Reports nondet-type fixes and validation results. |
| docs/reports/2026-10-11-handoff-v99-campanha.md | Records official campaign status and follow-ups. |
| docs/reports/2026-10-06-ambiente-testcomp-oficial.md | Documents the official validation environment. |
| docs/reports/2026-09-30-plano-campanha-completa.md | Defines full-campaign evaluation criteria. |
| docs/reports/2026-08-16-crabllvm-review.md | Adds historical invariant-engine findings. |
| docs/backlog.md | Tracks deferred studies and known limitations. |
| Dockerfile.dev | Clarifies Clam domain choices in comments only. |
| CMakeLists.txt | Sets the project version to 9.0.0. |
| CLAUDE.md | Documents hybrid state and invariant-study status. |
| CHANGELOG.md | Summarizes 9.0 behavior changes and fixes. |
Review effort: Balanced
| FLOAT = 16, | ||
| LONGLONG = 17, | ||
| ULONGLONG = 18, | ||
| U8 = 19, | ||
| U16 = 20, | ||
| U32 = 21, |
There was a problem hiding this comment.
Confirmed and fixed in 7db6dd5. It was wider than the new IDs: the parser knew 0–5 only, so 6–15 already left row.type uninitialized, and generateWitnessFunctionName() fell off the end of a non-void function. KleeLogType now mirrors enum NONDET_TYPE (0–22, same order) plus UNKNOWN for any other number or a non-numeric column; the generator name is a full table that always returns ("" for UNKNOWN). New unit test KleeLogTest (all 23 types, plus 23, -1, abc).
| llvm::FunctionCallee assume = F.getParent()->getOrInsertFunction( | ||
| "map2check_assume", llvm::Type::getVoidTy(ctx), int32); |
There was a problem hiding this comment.
Confirmed and fixed in 9c14229. Reproduced on the PR head: a function with abort() and __VERIFIER_assume produced map2check_assume.1, the native link failed (undefined reference to 'map2check_assume.1') and a reachable target answered UNKNOWN. adoptRuntimeName() now redirects the calls to the existing function when one of the same type already holds the runtime name, and renames otherwise; the nondet generators and the Crab assume use it too. The repro answers FAILED with input 42. Integration section 39.
| if (log.size > MAP2CHECK_KLEE_LOG_MAX_ROWS) | ||
| return; |
There was a problem hiding this comment.
Confirmed and fixed in c2bcc6c. Reproduced with 1100 nondet reads under --nondet-generator symex --generate-testcase: the PR head wrote long map2check_input_array[] = {}; (not even valid C). generateTestCase() now falls back to the violating .ktest (readViolatingKtest, as emitTestSuite does) when the log is empty, and writes {0} if nothing at all is recovered; the same repro writes all 1100 values. The costly symbolic CSV formatting stays off.
| } | ||
| if (i >= format.size()) break; | ||
| if (format[i] == 's' && !precision && !wide) positions.push_back(argument); | ||
| ++argument; |
There was a problem hiding this comment.
Confirmed and fixed in 223194e. With MAP2CHECK_CHECK_CSTRINGS=1, printf("%m %s %p", buf, (void *)&byte) on a safe program was a FALSE-DEREF on the PR head (it checked &byte as the string). %m now advances no argument; * width/precision are still counted. The safe program of integration section 36 now includes that call and stays TRUE.
| if echo "$output" | awk ' | ||
| /Exited fuzzer with/ { replay = 1 } | ||
| /Started Map2Check/ { replay = 0 } | ||
| /Segmentation fault|dumped core|Aborted \(core dumped\)/ { if (!replay) found = 1 } | ||
| END { exit !found }'; then |
There was a problem hiding this comment.
Confirmed and fixed in b25e6af. The outer exit status 139 (SIGSEGV) or 134 (SIGABRT) is now ERROR before the replay filter; definitive verdicts keep precedence. Three new cases in test_verdict_classifier.sh: tool SIGSEGV and SIGABRT after "Exited fuzzer with 0" are ERROR (both fail on the previous classifier), and the same log with exit 0 stays UNKNOWN.
| set -euo pipefail | ||
|
|
||
| INSTALL=${1:?install directory} | ||
| OUT=${2:?output zip} |
There was a problem hiding this comment.
Confirmed and fixed in deea32a. Both arguments are resolved to absolute paths before anything else, and a missing output directory is an error up front (exit 2). Checked: a relative rel.zip is now written in the caller's directory, and /nonexistent/x.zip stops with "no such directory" without touching anything.
hbgit
left a comment
There was a problem hiding this comment.
Revise tambem os comentarios do Github Pilot
| # KLEE runs to its window and only the fuzzer stops on stagnation. | ||
| find_package(Threads REQUIRED) | ||
| target_link_libraries(map2check Threads::Threads) | ||
| find_package(SQLite3) |
There was a problem hiding this comment.
Confirmar se ao final da verificação o DB é deletado
There was a problem hiding this comment.
Confirmado: sim. O banco SQLite é o run.stats que o KLEE grava no próprio diretório de saída (klee-out-N), dentro do diretório de trabalho <hash>.map2check; o Map2Check só o lê, durante a alternância, para detectar estagnação. No fim, Caller::cleanGarbage() apaga o diretório de trabalho inteiro (com o klee-out-N e o run.stats dentro), e o SeedStoreCleanup do main() apaga as pastas que ficam ao lado dele entre as fases (<hash>.seeds, <hash>.slice, <hash>.build) em qualquer saída, inclusive exceção. Só o --debug mantém tudo, de propósito.
Verifiquei em duas execuções do modo padrão (alternância AFL++/KLEE, sem --debug): ao final só sobram o programa, o log e test-suite/, e não há nenhum run.stats em todo o sistema de arquivos. A exceção é uma execução morta por fora (por exemplo, pelo timeout do BenchExec), em que a limpeza não chega a rodar; na competição isso não pesa, porque o contêiner da execução é descartado.
| case 15: return {"non_det_double", sizeof(double), true, false}; | ||
| case 16: return {"non_det_float", sizeof(float), true, false}; | ||
| case 17: return {"non_det_longlong", sizeof(int64_t), false, false}; | ||
| case 18: return {"non_det_ulonglong", sizeof(uint64_t), false, true}; |
There was a problem hiding this comment.
Sim, para unsigned long long o KLEE tem suporte completo: o valor vira um objeto simbólico de 8 bytes (non_det_ulonglong), e o solver trabalha com vetores de bits de 64 bits. Testei com o KLEE sozinho (--nondet-generator symex) num programa que só alcança o erro com x == 18446744073709551000ULL: ele encontra o valor exato.
O tipo vizinho, float (linha 84), é o caso delicado: o KLEE 3.1 não raciocina sobre ponto flutuante simbólico (silently concretizing (reason: floating point) ... to value 0). Por isso, nas tarefas de Floats, quem acha os erros é o AFL++ dentro do híbrido; o KLEE só reaproveita os valores concretos. A linha 84 continua necessária para decodificar os .ktest e as sementes.
|
|
||
| /** Marks a case as an assumption-pruned path (see evictPrunedCase). A comment | ||
| * is outside the test format, so validators and TestCov ignore it. */ | ||
| const char kPrunedComment[] = "<!-- map2check: assumption-pruned path -->"; |
There was a problem hiding this comment.
Feito em fb2423b: docs/test-suite-format.md documenta o formato das suítes como o Map2Check as escreve — arquivos, metadata.xml, ordem e codificação das entradas, o marcador <!-- map2check: assumption-pruned path --> (o que significa, por que os validadores o ignoram, e como a remoção de casos podados funciona quando a suíte enche), o limite de 50 casos e as duas fontes dos vetores (klee_log.csv, com a tabela de tipos, e o .ktest). O comentário do código e o cabeçalho de test_suite.hpp (que ainda dizia que Cover-Branches era trabalho futuro) apontam para lá.
There was a problem hiding this comment.
Gerado pelo tests/testcomp/build_corpus_official.py --property cover-branches --total 3500 (commit 0149e22): ele lê as categorias, com seus includesfile/excludesfile, da definição oficial da Test-Comp 2026, e sorteia de forma determinística e proporcional ao tamanho de cada categoria (ordem de bissecção aninhada, linhas intercaladas entre categorias).
Em fb2423b documentei a origem de todos os manifestos em tests/testcomp/corpus/README.md, com o comando e o commit de cada um. Conferi hoje: os oito regeneram idênticos, byte a byte, a partir do sv-benchmarks 7efe28dd2 (o commit fixado pela Test-Comp 2026).
KleeLogHelper::getListLogFromCSV() knew the type numbers 0-5 only. For any other -- 6-15 already, and the 16-22 of issue #74 -- it left row.type uninitialized, and generateWitnessFunctionName(), which the witness reads for every edge, fell off the end of a non-void function: undefined behaviour on a violation that involves one of those types (review of #73). KleeLogType now mirrors the runtime's enum NONDET_TYPE, in its order, plus UNKNOWN for any other number or a type column that is not a number; the generator name is a full table that always returns, "" for UNKNOWN. New unit test KleeLogTest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…naming
The abort() rewrite declares map2check_assume. When the same function also
calls the program's own __VERIFIER_assume, NonDetPass then renamed that
declaration onto the occupied name, and LLVM made it map2check_assume.1, a
symbol the runtime does not define: the native builds failed to link
("undefined reference to `map2check_assume.1'") and a reachable target came
back UNKNOWN (review of #73). Reproduced on install_v99c.
adoptRuntimeName() redirects the calls to the existing function when one of
the same type already has the name, and renames otherwise; the nondet
generators and the Crab assume use it too. The repro now answers FAILED with
the input 42. Integration section 39.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--generate-testcase built map2check_input_array from klee_log.csv only. Past 1024 nondet reads the KLEE runtime writes no log (issue #75), and an aborted KLEE state writes none either: the array came out empty -- `{}`, not even valid C -- and the testcase replayed nothing (review of #73). It now takes the violating .ktest's vector when the log is empty, as the Test-Comp suite already does, and writes `{0}` when there is nothing at all. Measured on 1100 nondet reads with --nondet-generator symex: install_v99c writes an empty array; this writes the 1100 values. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With MAP2CHECK_CHECK_CSTRINGS=1 the format parser counted %m (strerror of
errno) as a conversion with an argument, so every argument after it shifted
by one: printf("%m %s %p", buf, &byte) checked the one-byte &byte as the %s
string and reported a FALSE-DEREF on a safe program (review of #73;
reproduced on install_v99c). %m now advances no argument; the * of a width or
precision is still counted. Integration section 36 prints that format in its
safe program.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The classifier ignores crash messages between "Exited fuzzer with" and the next phase: there the core dump is the program under test, replayed on the fuzzer's crash. It also hid a crash of Map2Check itself in that stretch (replay processing, suite generation, cleanup): a run whose outer process died on SIGSEGV was classified UNKNOWN, or TIMEOUT near the budget (review of #73). The outer exit status 139/134 now makes it ERROR before that filter; definitive verdicts keep precedence. Three new classifier cases; the two crash cases fail on the previous classifier. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ectory The final zip runs from inside the staging directory. With a relative output name it created the archive there, and the cleanup deleted it -- after `rm -f "$OUT"` had removed the caller's previous archive; a relative path with a directory failed outright (review of #73). Both arguments are now made absolute first, and a missing output directory is an error up front. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s built Review of #73 asked to document the test-suite format (the assumption-pruned marker in particular) and how cover-branches-3500.tsv was created. - docs/test-suite-format.md: the files, metadata, the order and encoding of the inputs, the assumption-pruned comment and why it is safe for the validators, the size cap and the two sources of the vectors (klee_log.csv with its type numbers, and the violating .ktest). The header of test_suite.hpp, which still said Cover-Branches was future work, now points there. - tests/testcomp/corpus/README.md: the command and commit of every manifest. All eight regenerate byte for byte from sv-benchmarks 7efe28dd2 (checked 2026-10-22). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Todas as 10 observações da revisão foram tratadas em 7 commits (7db6dd5..fb2423b); cada comentário tem a resposta com o commit e a evidência. Verificação do head atualizado (
Três defeitos foram reproduzidos na 9.0 anterior e corrigidos aqui: falha de link com 🤖 Generated with Claude Code |


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