Skip to content

Verify a compilation before generating its code, never beside it - #138

Merged
SwayamInSync merged 7 commits into
mainfrom
fix/verify-before-codegen
Oct 29, 2026
Merged

SwayamInSync merged 7 commits into
mainfrom
fix/verify-before-codegen

Conversation

@SwayamInSync

@SwayamInSync SwayamInSync commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

clang++ -fverify-contracts verified a translation unit on a std::async thread (startCppVerifyAsync) while the main thread generated its IR from the same ASTContext (BackendConsumer::HandleTranslationUnit). IR generation is not read-only on the AST: it fills caches of the ASTContext (getTypeInfo's map, the linkage cache) and of the SourceManager (its last file and line lookups), which the verifier reads. Concurrent insertion into a map that another thread reads is undefined behavior. A ThreadSanitizer clang compiling a file with contracts and templates (-g -c) reported races in 4 of 4 runs (found while making #136): CodeGen's debug locations against Verifier::sourceOf in SourceManager::getLineNumber, setLinkageForGV against ASTConverter::functionIdentity, and ASTContext::getTypeInfo against VType::fromQualType. #136's lock orders the verifier's own threads, not the verifier against CodeGen.

Change

runCppVerify(CI, Ctx) replaces startCppVerifyAsync and finishCppVerify: it verifies on the calling thread and reports the results through the compiler's diagnostics as before. BackendConsumer::HandleTranslationUnit calls it before Gen->HandleTranslationUnit, so nothing else touches the AST meanwhile. Verus runs its verification inside rustc's pipeline the same way, before code generation.

A failure is now reported before IR generation, which resets the module once an error is reported (CodeGeneratorImpl::HandleTranslationUnit), so a compilation that fails verification stops there. Before, IR generation had already released the module, and -emit-llvm -o - printed the IR of a program that failed verification.

The overlap bought nothing measurable: optimization and the backend already ran after verification finished, and IR generation itself is small. A whole -fno-verify compile of suite/llvm_uleb128.cpp takes 0.02 s, against seconds of verification.

The IO sandbox

In a build with assertions, LLVM turns on its IO sandbox (LLVM_ENABLE_IO_SANDBOX, which defaults to LLVM_ENABLE_ASSERTIONS) for the compiler's main thread (driver.cpp, Job.cpp): any file access outside the compilation's file system is a fatal error. The flag is thread-local, so the verifier's own thread never saw it; on the main thread, the first commit of this PR crashed every -fverify-contracts compile with fatal error: error in backend: IO sandbox violation, at the verifier's first read (compute_thread_count reading /proc/cpuinfo). The verifier reads and writes files of its own (the proof cache, the report, -fverify-contracts-out) and runs cvc5 and Lean, so runCppVerify lifts the sandbox for the whole call (llvm::sys::sandbox::scopedDisable), as the static analyzer does for its reports (ExprEngine.cpp, HTMLDiagnostics.cpp). The compile tests (compile_with_verify.cpp, compile_diagnostics.cpp, compile_options.cpp, ...) are what catch it, in the assertions job. Checked locally with a release build configured with -DLLVM_ENABLE_IO_SANDBOX=ON: the 8 compile-path tests pass; without the scopedDisable, compile_with_verify.cpp, compile_verifies_first.cpp, and compile_diagnostics.cpp fail with fatal error: error in backend: IO sandbox violation.

ThreadSanitizer

Checked with a ThreadSanitizer clang (-std=c++17 -fverify-contracts -g -c, Z3's own reports suppressed) on two probe files of 16 functions each, before this PR (main with #136) and with it, sandbox lifted:

Probe Before With this PR
16 functions that verify, so IR is generated races in 8 of 8 runs (82 to 229 warnings), object built 0 of 8, object built in about 3 s
16 functions that fail races in 12 of 12 runs (7 to 119 warnings) 0 of 8, 16 failures reported, no object

The races before were SourceManager::getLineNumber (CGDebugInfo::setLocation against Verifier::sourceOf) and setCachedLinkage (setLinkageForGV against ASTConverter::functionIdentity). The probe that verifies is the one that shows the race gone: the failing one never reaches IR generation now.

Testing

  • New compile_verifies_first.cpp: a failing function compiled with -emit-llvm -o - reports the failure and no define; with -fno-verify the IR is generated. On main it fails: CHECK-NOT: excluded string found in input, define dso_local noundef i32 @_Z4nexti(...).
  • Every test outside suite/ (81) and the compile-path tests in it (z3_references.cpp, ghost_locals.cpp) pass.
  • A data race is not deterministic in a release build; the ordering is what the test checks, and with it no verifier code runs beside IR generation.

Docs

ARCHITECTURE.md ("Compile-Time Integration") says verification runs first and why; release notes ("Running cpp-verify", "Verification before code generation").

@SwayamInSync
SwayamInSync merged commit 926f5d8 into main Oct 29, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant