Z3 Bug Report: sat.smt=true returns invalid model for BV comparator assertions
Component: smt.elim_unconstrained + sat.smt=true witness reconstruction
Affected versions: Z3 4.16.0, Z3 main HEAD (4.17.0-dev)
Regression: Z3 4.13.0 is not affected
Option: sat.smt=true (non-default, opt-in)
Type: invalid_model — verdict SAT is correct, but the returned model violates the asserted formula
Workaround: add smt.elim_unconstrained=false
Summary
When sat.smt=true is enabled, Z3 can return a model that falsifies the asserted formula. The verdict (sat) is correct — satisfying assignments exist — but the witness reconstruction assigns a value that violates one or more constraints.
The bug affects boolean structures of bitvector comparator predicates (bvuge, bvult, bvugt, bvule) and their boolean combinations (and, or, xor, not). It is triggered by smt.elim_unconstrained, which abstracts comparator sub-formulas to fresh Boolean variables; the subsequent witness reconstruction independently satisfies each predicate without ensuring joint consistency.
Minimal Reproducer
Pattern 1 — AND scalar inclusion (simplest)
(set-logic QF_BV)
(declare-const k (_ BitVec 4))
(assert (and (bvuge k (_ bv1 4)) (bvult k (_ bv3 4))))
(check-sat)
(get-model)
Expected: sat with k ∈ {1, 2} (any value satisfying 1 ≤ k < 3)
Actual (z3 sat.smt=true): sat, k = #x0 (= 0)
Verification: bvuge(0, 1) = false → returned model violates bvuge k 1
Pattern 2 — OR range rejection
(set-logic QF_BV)
(declare-const x (_ BitVec 4))
(assert (or (bvult x (_ bv1 4)) (bvuge x (_ bv7 4))))
(check-sat)
(get-model)
Expected: sat with x ∈ {0} ∪ {7, 8, …, 15}
Actual (z3 sat.smt=true): sat, x = #x6 (= 6)
Verification: bvult(6,1) = false, bvuge(6,7) = false → or(false, false) = false → invalid model
Pattern 3 — XOR of two comparators
(set-logic QF_BV)
(declare-const v1 (_ BitVec 3))
(declare-const v2 (_ BitVec 3))
(assert (xor (bvult v2 (_ bv6 3)) (bvult (_ bv4 3) v1)))
(check-sat)
(get-model)
Expected: sat with e.g. v1 = 7, v2 = 0 (where exactly one predicate holds)
Actual (z3 sat.smt=true): sat, v1 = #b101 (5), v2 = #b101 (5)
Verification: bvult(5,6) = true, bvult(4,5) = true → xor(true, true) = false → invalid model
Pattern 4 — Double negation (NOT NOT)
(set-logic QF_BV)
(declare-const x (_ BitVec 3))
(assert (not (not (and (bvuge x (_ bv1 3)) (bvult x (_ bv3 3))))))
(check-sat)
(get-model)
Expected: sat with x ∈ {1, 2}
Actual (z3 sat.smt=true): sat, x = #b000 (= 0)
Verification: bvuge(0, 1) = false → invalid model
Note: Double negation does not protect against the bug; the inner AND form is still abstracted.
Pattern 5 — Multi-assertion (variable appears in two assertions)
(set-logic QF_BV)
(declare-const x (_ BitVec 4))
(assert (= (bvand x (_ bv3 4)) x))
(assert (and (bvuge x (_ bv1 4)) (bvult x (_ bv3 4))))
(check-sat)
(get-model)
Expected: sat with x ∈ {1, 2} (satisfies both assertions)
Actual (z3 sat.smt=true): sat, x = #x0 (= 0)
Verification: bvuge(0, 1) = false → second assertion violated
Note: x appears in both assertions. The bug fires despite the variable not being "single-occurrence". smt.elim_unconstrained abstracts x from the comparator predicates in the second assertion even though x appears in the first assertion as well.
Metamorphic Confirmation (no external oracle required)
The following two formulas are semantically equivalent by De Morgan's law:
v1: (or (bvult x 1) (bvuge x 7))
v2: (not (and (bvuge x 1) (bvult x 7)))
Both express x ∉ [1, 6]. Any sound solver must return consistent models for both.
v1 (z3 sat.smt=true):
; v1-or-range.smt2
(set-logic QF_BV)
(declare-const x (_ BitVec 4))
(assert (or (bvult x (_ bv1 4)) (bvuge x (_ bv7 4))))
(check-sat)
(get-model)
Expected: sat with x ∈ {0} ∪ {7..15}
Actual: sat, x = #x6 (= 6) — violates v1
v2 (z3 sat.smt=true):
; v2-not-and-range.smt2
(set-logic QF_BV)
(declare-const x (_ BitVec 4))
(assert (not (and (bvuge x (_ bv1 4)) (bvult x (_ bv7 4)))))
(check-sat)
(get-model)
Expected: same satisfying set as v1
Actual: sat, x = #x0 (= 0) — satisfies v2, but inconsistent with v1's model
Result: v1 and v2 are equivalent yet produce different models (6 vs 0). Cross-validating v1's model (x=6) against v2: not(and(bvuge(6,1), bvult(6,7))) = not(and(T,T)) = false → v1's model falsifies v2. This proves the bug without any external oracle.
Root Cause
The interaction between smt.elim_unconstrained and sat.smt=true:
-
smt.elim_unconstrained identifies variables that appear (at least once) only in comparator predicates and abstracts each predicate to a fresh Boolean variable:
b1 ← (bvuge k 1), b2 ← (bvult k 3)
-
The SAT layer satisfies the resulting Boolean formula (e.g., and(b1, b2) → b1=T, b2=T).
-
Witness reconstruction maps each Boolean back to a BV value independently:
b1=T → pick k such that bvuge k 1 → selects k = 0 (off-by-one error: picks lo − 1)
b2=T → pick k such that bvult k 3 → k = 0 happens to satisfy this too
-
The independently chosen values are combined into a single model, but their joint consistency is not checked. Result: k = 0 which falsifies b1.
Workaround: smt.elim_unconstrained=false disables the abstraction step and eliminates all observed instances of the bug.
Z3 Self-Diagnosis
With model_validate=true, Z3 confirms the bug itself on all 5 patterns:
$ z3 sat.smt=true model_validate=true minimal/01-and-scalar.smt2
sat
(error "line 10 column 10: an invalid model was generated")
(
(define-fun k () (_ BitVec 4)
#x0)
)
The same an invalid model was generated error appears for patterns 1–5.
Regression Window
| Version |
Commit / Hash |
sat.smt=true result |
Correct? |
| Z3 4.8.7 |
— |
k = #x1 |
✓ |
| Z3 4.13.0 |
— |
k = #x1 |
✓ |
| Z3 4.16.0 |
— |
k = #x0 |
✗ BUG |
| Z3 main HEAD |
b9109f031e64f6ab2e3faf4b795968e6f2c0ef4d |
k = #x0 |
✗ BUG |
Z3 4.16.0 (default, no sat.smt) |
— |
k = #x1 |
✓ |
Z3 4.16.0 + smt.elim_unconstrained=false |
— |
k = #x1 |
✓ |
Cross-Solver Verification
All reference solvers return correct models on the same formula:
| Solver |
Model |
Correct? |
| cvc5 (latest) |
k = 1 or k = 2 |
✓ |
| Bitwuzla (latest) |
k = 1 |
✓ |
| Yices 2.7.0 |
k = 1 |
✓ |
| Z3 4.16.0 (default) |
k = 1 |
✓ |
Files
report_bug/
├── REPORT.md ← this file
├── repro.sh ← automated reproducer (run with: bash repro.sh)
├── minimal/
│ ├── 01-and-scalar.smt2 ← Pattern 1: AND scalar inclusion
│ ├── 02-or-range.smt2 ← Pattern 2: OR range rejection
│ ├── 03-xor-cmp.smt2 ← Pattern 3: XOR comparators
│ ├── 04-double-neg.smt2 ← Pattern 4: double negation wrapper
│ └── 05-multi-assert.smt2 ← Pattern 5: multi-assertion trigger
└── metamorphic/
├── v1-or-range.smt2 ← DeMorgan pair v1
└── v2-not-and-range.smt2 ← DeMorgan pair v2 (equivalent to v1)
Run bash repro.sh to verify all patterns in one shot. Set Z3=/path/to/z3 if needed.
repro.zip
@yoonsh
Z3 Bug Report:
sat.smt=truereturns invalid model for BV comparator assertionsComponent:
smt.elim_unconstrained+sat.smt=truewitness reconstructionAffected versions: Z3 4.16.0, Z3 main HEAD (4.17.0-dev)
Regression: Z3 4.13.0 is not affected
Option:
sat.smt=true(non-default, opt-in)Type:
invalid_model— verdict SAT is correct, but the returned model violates the asserted formulaWorkaround: add
smt.elim_unconstrained=falseSummary
When
sat.smt=trueis enabled, Z3 can return a model that falsifies the asserted formula. The verdict (sat) is correct — satisfying assignments exist — but the witness reconstruction assigns a value that violates one or more constraints.The bug affects boolean structures of bitvector comparator predicates (
bvuge,bvult,bvugt,bvule) and their boolean combinations (and,or,xor,not). It is triggered bysmt.elim_unconstrained, which abstracts comparator sub-formulas to fresh Boolean variables; the subsequent witness reconstruction independently satisfies each predicate without ensuring joint consistency.Minimal Reproducer
Pattern 1 — AND scalar inclusion (simplest)
Expected:
satwithk ∈ {1, 2}(any value satisfying1 ≤ k < 3)Actual (
z3 sat.smt=true):sat,k = #x0(= 0)Verification:
bvuge(0, 1) = false→ returned model violatesbvuge k 1Pattern 2 — OR range rejection
Expected:
satwithx ∈ {0} ∪ {7, 8, …, 15}Actual (
z3 sat.smt=true):sat,x = #x6(= 6)Verification:
bvult(6,1) = false,bvuge(6,7) = false→or(false, false) = false→ invalid modelPattern 3 — XOR of two comparators
Expected:
satwith e.g.v1 = 7, v2 = 0(where exactly one predicate holds)Actual (
z3 sat.smt=true):sat,v1 = #b101(5),v2 = #b101(5)Verification:
bvult(5,6) = true,bvult(4,5) = true→xor(true, true) = false→ invalid modelPattern 4 — Double negation (NOT NOT)
Expected:
satwithx ∈ {1, 2}Actual (
z3 sat.smt=true):sat,x = #b000(= 0)Verification:
bvuge(0, 1) = false→ invalid modelNote: Double negation does not protect against the bug; the inner AND form is still abstracted.
Pattern 5 — Multi-assertion (variable appears in two assertions)
Expected:
satwithx ∈ {1, 2}(satisfies both assertions)Actual (
z3 sat.smt=true):sat,x = #x0(= 0)Verification:
bvuge(0, 1) = false→ second assertion violatedNote:
xappears in both assertions. The bug fires despite the variable not being "single-occurrence".smt.elim_unconstrainedabstractsxfrom the comparator predicates in the second assertion even thoughxappears in the first assertion as well.Metamorphic Confirmation (no external oracle required)
The following two formulas are semantically equivalent by De Morgan's law:
Both express
x ∉ [1, 6]. Any sound solver must return consistent models for both.v1 (
z3 sat.smt=true):Expected:
satwithx ∈ {0} ∪ {7..15}Actual:
sat,x = #x6(= 6) — violates v1v2 (
z3 sat.smt=true):Expected: same satisfying set as v1
Actual:
sat,x = #x0(= 0) — satisfies v2, but inconsistent with v1's modelResult: v1 and v2 are equivalent yet produce different models (6 vs 0). Cross-validating v1's model (x=6) against v2:
not(and(bvuge(6,1), bvult(6,7))) = not(and(T,T)) = false→ v1's model falsifies v2. This proves the bug without any external oracle.Root Cause
The interaction between
smt.elim_unconstrainedandsat.smt=true:smt.elim_unconstrainedidentifies variables that appear (at least once) only in comparator predicates and abstracts each predicate to a fresh Boolean variable:b1 ← (bvuge k 1),b2 ← (bvult k 3)The SAT layer satisfies the resulting Boolean formula (e.g.,
and(b1, b2)→b1=T, b2=T).Witness reconstruction maps each Boolean back to a BV value independently:
b1=T→ pickksuch thatbvuge k 1→ selectsk = 0(off-by-one error: pickslo − 1)b2=T→ pickksuch thatbvult k 3→k = 0happens to satisfy this tooThe independently chosen values are combined into a single model, but their joint consistency is not checked. Result:
k = 0which falsifiesb1.Workaround:
smt.elim_unconstrained=falsedisables the abstraction step and eliminates all observed instances of the bug.Z3 Self-Diagnosis
With
model_validate=true, Z3 confirms the bug itself on all 5 patterns:The same
an invalid model was generatederror appears for patterns 1–5.Regression Window
sat.smt=trueresultk = #x1k = #x1k = #x0b9109f031e64f6ab2e3faf4b795968e6f2c0ef4dk = #x0sat.smt)k = #x1smt.elim_unconstrained=falsek = #x1Cross-Solver Verification
All reference solvers return correct models on the same formula:
k = 1ork = 2k = 1k = 1k = 1Files
Run
bash repro.shto verify all patterns in one shot. SetZ3=/path/to/z3if needed.repro.zip
@yoonsh