Observed behaviour matches more than one of disjoint behaviours
| Vulnerability potential | Low |
| DDoS potential | None |
Observed behaviour is at the intersection of two or more behaviour preconditions
Impact
A disjoint behaviours clause asserts that the named behaviours are mutually
exclusive: no admissible input can match more than one assumes guard. A
violation means the analyzer found an input that satisfies two (or more) guards
at once. The behaviours then disagree about what the function should do for that
input — each promises its own ensures — so the specification is internally
contradictory at the overlap. If the two postconditions are compatible the
spec is merely redundant, but if they differ, one of them must be false for that
input, which makes the contract unsatisfiable and any proof built on it suspect.
It also signals that the author’s mental case split was wrong.
Vulnerability potential
This is a specification-consistency defect with no direct security impact. The risk is that a contradictory contract can let a verifier “prove” properties vacuously or that an implementation guided by an ambiguous spec handles the overlapping case inconsistently across versions, eroding trust in the verification result. There is no attacker-controlled memory or control-flow consequence inherent to the overlap itself; resolve it to keep the proof sound, not to close an exploit.
Technical details
In ACSL a function contract can be partitioned into named behavior blocks,
each guarded by an assumes predicate. The disjoint behaviors meta-clause
demands that the guards never overlap; its dual, complete behaviors, demands
they leave no gap. Together they state that the guards form an exact partition of
the input space.
The verifier reduces disjointness to proving, for every pair, that
!(assumes_i && assumes_j) under the precondition, and reports a violation when
it finds a model satisfying both. The classic cause is using non-strict
relational operators on both sides of a boundary — covering x <= 0 and
x >= 0 makes x == 0 belong to both — or guards on independent conditions
that the author wrongly assumed were exclusive. The fix is to tighten one guard
(make a bound strict, or add a conjunct) so the cases separate cleanly. Note that
disjointness and completeness pull in opposite directions at boundaries: fixing
an overlap by making a bound strict can open a completeness gap and vice versa,
so both clauses should be checked together.
Catching the issue
Run Frama-C/WP with the disjoint behaviors clause; it produces a dedicated
disjointness goal per pair of behaviours and reports the unproven one with a
witness input. Make that goal a CI gate. When writing the guards, use a
consistent convention at every boundary — one side strict, the other non-strict
(x < 0 vs x >= 0) — so adjacent cases meet exactly once. Always declare
complete and disjoint together and verify both, since checking only one
lets the other kind of defect through.
How to reproduce
Observe that x == 0 satisfies both guards (x <= 0 and x >= 0);
frama-c -wp reports the disjoint behaviors goal as unproven, and the two
behaviours give contradictory results there.
/*@ behavior nonpos:
@ assumes x <= 0; // overlaps nonneg at x == 0
@ ensures \result == 0;
@ behavior nonneg:
@ assumes x >= 0; // overlaps nonpos at x == 0
@ ensures \result == x;
@ disjoint behaviors nonpos, nonneg; // fails: x == 0 matches both
@*/
int clamp_low(int x) {
return x < 0 ? 0 : x;
}