Map2Check 9.0.0: AFL++ engine, smart seeds, engine alternation, sound TRUE verdicts - #73
GuilhermeBn198 wants to merge 57 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>
The Cover-Branches suite came only from KLEE's paths; the fuzzer's queue -- the inputs that reached new edges -- was thrown away with the scratch directory. With MAP2CHECK_FUZZER_SUITE=1 a fuzzer phase replays its ranked queue through the witness binary for the values each entry reads and writes them as test cases, up to half the suite so KLEE's paths keep room, without duplicates. Off by default until measured (R22). 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's partial paths -- the states it dumped at a halt -- stop where the search stopped. Run natively through the fuzzer's witness binary, completed with zeros past their end, some of them go on to the error: that is how the seed-exchange hybrid covered eca-* tasks KLEE left UNKNOWN, by accident, in its last fuzzer phase's dry run. Now done on purpose right after a KLEE phase that found nothing (not for Cover-Branches), within 10% of the budget, with the witness from the AFL++ build cache -- so the plain hybrid gets it too. A violation's files become the phase's, as a confirmed fuzzer crash's do. eca-rers2012/Problem06_label05, plain hybrid: UNKNOWN -> FAILED at 207 s (1850 vectors replayed in 15 s). Measured in R24. 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 harness reads its manifest on fd 3, and every child inherited it: map2check, KLEE, and through KLEE's native external calls (read, lseek, close on a numeric fd) the program under test itself. The shared offset moved under the loop: a shard ended after 29 of 71 tasks with no deadline reached, and another wrote 38 rows belonging to the next shard (R19, R24). The children now run with fd 3 closed. 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>
GenHash read an unreadable file's size as -1 and std::vector rejected it with a bare 'cannot create std::vector larger than max_size()': all 120 tasks of a Cover-Branches arm came back ERROR with that line during a memory-exhausted spell of the R22 run. The hash now fails, and the Caller reports the file. 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>
…eally was Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…mization The --add-invariants study (INV-1 in the experiment log) found that the crab-llvm of the SV-COMP 2019/2020 builds emitted llvm.assume, which neither NonDetPass nor KLEE consumes, and that on a loop program the gain of going through Clam came from its preprocessing, not from the invariants: with none inserted it decided a safe and a buggy variant the plain pipeline left UNKNOWN. - MAP2CHECK_CLAM_PROFILE=default|memory|none: the current Clam flags, the 2018 configuration that did reach KLEE (memory tracked, invariants after loads), or Clam's pipeline with no invariant. - The number of invariants Clam inserted is logged, and a Clam failure falls back to the plain compile with a warning instead of leaving no bitcode. - MAP2CHECK_PREOPT=ssa: the module in SSA form (mem2reg, simplifycfg without sinking) for reachability and assert only -- the memory modes track locals through the allocas mem2reg removes (FALSE-FREE on safe programs), overflow lost detections, and fewer branches mean fewer Cover-Branches cases. Integration 50/50 with it off, 50/50 with it on (section 32 flaky: 3/3 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>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A major version: AFL++ replaces LibFuzzer (--nondet-generator fuzzer is now afl), and TRUE now requires an exhaustive KLEE exploration while the program's abort() prunes a path -- the same program can get another answer than under 8.x. The changelog lists the whole TACAS line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Partial update (R21 CASTLE): with the |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Partial update, complete rounds on the 213-task Cover-Error sample:
|
On the eca-* and product-lines programs Clam's analysis ran past the whole budget and the run was killed with no verdict (R25: 4 to 7 ERROR per --add-invariants arm). Clam now gets at most 0.2T; past it the existing fallback compiles the program without invariants (Problem08_label51: ERROR -> UNKNOWN at 277 s). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Overnight results, all complete. Numbers are covered tasks out of 213 for Cover-Error and mean coverage over 120 tasks for Cover-Branches. Cover-Error (reference R19 control: 128; v15 on the same sample: 111)
Cover-Branches (reference R19 control: 44.7%)
Consolidated run (R26) now running on this branch (a1506bc):
It will decide the default hybrid. |
|
R26 (consolidated, this branch at a1506bc), first complete arm: Cover-Error with |
|
R26, Cover-Error with |
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, 52 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=ssaWhy 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.Smaller fixes
while (nondet())loop never ended and afl-fuzz aborted in its dry run.Results
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 variantsCover-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.
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, which is why it is off by default.Tests
ctest). New:AlternationTest, plus new cases inKtestReaderTest,SlicerTest,SeedStoreTestandTestSuiteTest.tests/integration/test_testcomp_regressions.sh): 50/50 on the tip, 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).Still running
These rounds run on frozen installs. I will post each result in this PR as it completes:
%scheck);--add-invariantsprofiles andMAP2CHECK_PREOPT=ssaagainst off).Before leaving draft
--seed-exchangeor--alternate-engines;MAP2CHECK_FUZZER_SUITE;MAP2CHECK_PREOPT=ssa;MAP2CHECK_CHECK_CSTRINGS.🤖 Generated with Claude Code