Conversation
Clang lowers va_arg inline, walking the target's va_list layout, but llvm.va_start reaches Boogie as a bodyless declaration, so the list is never initialized and every va_arg reads unconstrained memory. A program whose result depends on a variadic argument therefore cannot be verified: `first(1, 42) == 42` is reported as an error today, and under the SV-COMP frontend __builtinx_va_arg answers 0 instead, which misses bugs rather than inventing them. The new pass supplies the missing half, behind --lower-varargs. For each direct call to a defined variadic function it clones the callee with the variadic arguments as ordinary parameters, and in the clone replaces llvm.va_start with code that lays those arguments out where the lowering will look for them: a buffer of one eight-byte slot per argument, with the list's overflow_arg_area pointing at it and both register-save offsets exhausted, so the lowering takes the overflow path whatever the argument's type. Clang's own va_arg code then reads the real values, and nothing has to recognize its shape. Putting the values in memory rather than in the clone's parameters is what makes a va_list handed to another function work, as vfprintf does; an approach that resolved va_arg to the k-th parameter could not, since the callee cannot evaluate its caller's parameters. Ten regressions cover the value, a loop over several arguments, argument order, a nondeterministic value, forwarding, a pointer argument and a floating-point one; six of them are the failing twin of another, so none of this passes by doing nothing. With the option off the emitted Boogie is byte-identical to develop over 674 translations. Two limits, both refusals rather than wrong answers, and both leaving the call exactly as it is today: a variadic list mixing floating-point with integer or pointer arguments, because one buffer holding both collapses its sea-DSA region and a float access to a region that is not typed float goes through SMACK's approximate "unsafe" path; and arguments passed by value whose ABI classification this pass does not reproduce. The slot layout is the x86-64 overflow area, which is the only layout SMACK's pinned target uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shaobo-he
force-pushed
the
va-arg-lowering
branch
from
August 27, 2026 21:42
17f773e to
bc634ad
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
va_argreads unconstrained memory today, so a program whose result depends on a variadic argument cannot be verified. This models it precisely, behind--lower-varargs.The problem
Clang lowers
va_arginline, walking the target'sva_list—{ i32 gp_offset; i32 fp_offset; i8* overflow_arg_area; i8* reg_save_area; }— butllvm.va_startreaches Boogie as a bodyless declaration:The list is never initialized, so every
va_argreads an unconstrained value. On develop,__VERIFIER_assert(first(1, 42) == 42)is reported as an error, andsum(3,10,20,30) == 60"verifies" only at--unroll=1, vacuously — at--unroll=5it errors.The SV-COMP frontend takes a different route:
#define __builtin_va_arg(ap, t) __builtinx_va_arg(ap), whose body isreturn 0;. That direction misses bugs instead of inventing them.Neither path is pinned by a test.
test/c/basic/vararg{,_fail}.cassert only on the named parameter, so both pass whateverva_argreturns; one of them even reads a variadic argument that was never passed.The approach
The groundwork was already there: SMACK monomorphizes variadic callees per call-site signature, so the callee already receives the value —
procedure first.i32.i32($i0: i32, p.1: i32), called asfirst.i32.i32(1, 42). The body just ignoresp.1and does the ABI dance instead.The new pass connects them. For each direct call to a defined variadic function it clones the callee with the variadic arguments as ordinary parameters, and in the clone replaces
llvm.va_startwith code that lays those arguments out where the lowering will look for them: one eight-byte slot per argument,overflow_arg_areapointing at the buffer, and both register-save offsets exhausted (gp_offset = 48,fp_offset = 304) so the lowering takes the overflow path whatever the argument's type. Clang's ownva_argcode then reads the real values.Nothing has to recognize the shape of that lowering, which is the point: the fix is four stores at
va_start, and every corner of clang's code — the register/overflow branch, the+8walk,va_copy— keeps working as written.Putting the values in memory rather than in the clone's parameters is what makes a
va_listhanded to another function work, asvfprintfdoes. The obvious alternative — resolveva_argto the k-th parameter with anif-chain — cannot do that, because the callee cannot evaluate its caller's parameters.Results
Corral, all three memory models:
va_argin a loop over three argumentsva_listforwarded to another functiondoubleargument_failtwins of the abovebasic/vararg{,_fail}.cVerification
test/c/varargs, exhaustive (three memory models × Boogie + Corral): 60 passed, 0 failed. Six of the ten are the failing twin of another, so nothing here passes by doing nothing. Added to the CI matrix.basic,memory-safety,data,strings,bitsandfloat× two memory models, 0 different. The pass is only added to the pipeline when the option is given.Limits
Both are refusals that leave the call exactly as it is today, never a wrong answer:
$store.unsafe.*path, so the value does not come back out. Separating those arguments needs storage the ABI lowering would still have to walk contiguously.The slot layout is the x86-64 overflow area, which is the layout SMACK's pinned target uses; a target guard belongs here before this is more than opt-in.
Out of scope, and unchanged: #55 (function pointers to variadic functions) and #252 (
invoketo a variadic function) — the pass specializes direct calls only. If this lands,__builtinx_va_argcould be deleted rather than kept as a second, differently-wrong model.🤖 Generated with Claude Code