Browse WireGuard specs (31 of 38)
Proofs / WireGuard / Earlier, longer properties
The replay window
Over any sequence of 64-bit nonces, the data path's replay window counter_validate accepts a nonce exactly when it is below the message limit, within 8128 of the highest accepted nonce, and not accepted before.
- counter_validate is copied out of receive.c byte for byte, since the rest of receive.c is not compiled.
- spec
- x_counter
- length
- 83 lines of C
- own proof
- 600 lines of Rocq
- status
- Proved
- 31 of 38
On this page
The spec
The property is stated by the C program harness/x_counter.c, 83 lines. This is its main (lines 27–83):
int main(void)
{
struct noise_replay_counter c;
struct acc *set = NULL, *a, **l;
u64 top = 0, n;
size_t steps, i;
int in, ok;
bool r;
c.counter = 0;
for (i = 0; i < COUNTER_BITS_TOTAL / BITS_PER_LONG; i++)
c.backtrack[i] = 0;
steps = draw_steps();
for (i = 0; i < steps; i++) {
switch (nondet_u8() % 5) {
case 0: n = top + (u64)(nondet_u32() % 200) - 100; break; // around the top
case 1: n = top - (u64)(nondet_u32() % 9000); break; // inside and just outside the window
case 4: n = top - M_W - 1 + (u64)(nondet_u32() % 5) - 2; break; // the window's edge
case 2: n = M_R + (u64)(nondet_u32() % 8) - 4; break; // around the limit
default: n = draw_u64(); break;
}
r = wg_counter_validate(&c, n);
in = 0;
for (a = set; a; a = a->next)
if (a->n == n)
in = 1;
ok = top < M_R + 1 && n < M_R && n + 1 + M_W >= top && !in;
CHECK(r == ok);
if (ok) {
a = malloc(sizeof(*a));
if (a == NULL)
break;
a->n = n;
a->next = set;
set = a;
if (n + 1 > top)
top = n + 1;
}
CHECK(c.counter == top);
// forget the nonces the window has left behind (the model accepts none of them again anyway)
for (l = &set; *l;) {
if ((*l)->n + 1 + M_W < top) {
a = *l;
*l = a->next;
free(a);
} else {
l = &(*l)->next;
}
}
}
while (set) {
a = set->next;
free(set);
set = a;
}
return 0;
}The full spec at commit 3c1fc13 ↗
The property as the spec's first comment states it
Law `counter`: the data-path replay window counter_validate (drivers/net/wireguard/receive.c, RFC 6479,
extracted unchanged in patch/counter_validate.c) against a shadow model, over ANY sequence of counters.
A fresh replay counter (zeroed, as kzalloc'd in keypair_create) and a sequence of ANY length of ANY 64-bit
nonces (the harness also draws them near the highest accepted one and near REJECT_AFTER_MESSAGES).
Shadow model: the set A of accepted nonces and top = 1 + the largest accepted nonce (0 if none). With
W = COUNTER_WINDOW_SIZE = 8192 - 64 = 8128 and R = REJECT_AFTER_MESSAGES = 2^64 - 1 - W - 1 (computed here, not
taken from messages.h), nonce n is accepted exactly when
top < R + 1 and n < R and n + 1 + W >= top and n is not in A;
then n joins A and top becomes max(top, n + 1).
Claims after every call: the return value is the model's, and counter->counter == top. (In words: every
nonce is accepted at most once, nonces below R only, nothing older than the window of W nonces behind the
highest, and everything else that is new is accepted: the bitmap never forgets a nonce inside the window.)The code it is about
counter_validate in patch/counter_validate.c, lines 20–58:
static bool counter_validate(struct noise_replay_counter *counter, u64 their_counter)
{
unsigned long index, index_current, top, i;
bool ret = false;
spin_lock_bh(&counter->lock);
/* QUICKPROOF P10: `goto out` is `break` out of this block (VST has no goto); same statements in the same order */
do {
if (unlikely(counter->counter >= REJECT_AFTER_MESSAGES + 1 ||
their_counter >= REJECT_AFTER_MESSAGES))
break;
++their_counter;
if (unlikely((COUNTER_WINDOW_SIZE + their_counter) <
counter->counter))
break;
index = their_counter >> ilog2(BITS_PER_LONG);
if (likely(their_counter > counter->counter)) {
index_current = counter->counter >> ilog2(BITS_PER_LONG);
top = min_t(unsigned long, index - index_current,
COUNTER_BITS_TOTAL / BITS_PER_LONG);
for (i = 1; i <= top; ++i)
counter->backtrack[(i + index_current) &
((COUNTER_BITS_TOTAL / BITS_PER_LONG) - 1)] = 0;
WRITE_ONCE(counter->counter, their_counter);
}
index &= (COUNTER_BITS_TOTAL / BITS_PER_LONG) - 1;
ret = !test_and_set_bit(their_counter & (BITS_PER_LONG - 1),
&counter->backtrack[index]);
} while (0); /* QUICKPROOF P10: was the label `out:` */
spin_unlock_bh(&counter->lock);
return ret;
}This is the code under test as the proof sees it: Linux at 551c722f with the two lib/crypto patches, and with the idiom rewrites that VST needs, each marked QUICKPROOF in the code and listed in DEVIATIONS.md.
The theorem
QuickProof generated this statement from the spec in src/contracts/x_counter.v:
Theorem x_counter : harness_safe whole_prog.whole_prog is the spec linked with the code under test. The 19 x_ specs are proved together, as the theorem suite_x, from one specification of every function of the code. src/contracts/laws.v states each spec on its own program and takes it from suite_x:
Theorem x_counter : harness_safe WgNoise.contracts.x_counter.whole_prog.
Proof. exact (proj1 (proj2 (proj2 (proj2 (proj2 (proj2 WgNoise.contracts.suite.suite_x)))))). Qed.qp verify checks that this statement is the one generated from the spec at the contract commit, and that its proof uses no axiom beyond the 13 that every spec here uses: standard logical axioms and VST's abstract malloc library.
The C files (units) its program links
drivers/net/wireguard/noise.cdrivers/net/wireguard/cookie.cpatch/counter_validate.clib/crypto/blake2s.clib/crypto/chacha.clib/crypto/chacha-block-generic.clib/crypto/poly1305.clib/crypto/poly1305-donna32.clib/crypto/chacha20poly1305.clib/crypto/curve25519.clib/crypto/curve25519-fiat32.cpatch/wg_shim.cquickproof/libc/libc.cquickproof/libc/malloc_unit.c
What it leaves out
- Like every spec of this proof: runs of more than one thread (locks and RCU are those of a single thread), termination (a run that never returns is not a counterexample), and binaries built by a compiler (the theorem is about the code as CompCert defines C for 64-bit x86).
- The kernel around the code is a shim of plain C for one thread, and the code carries rewrites of idioms that VST cannot handle. Both are tested against the kernel's code, not proved.
The summary page lists what the proof as a whole does not cover.
How it was proved
src/proof/harness/counter/ holds 600 lines of Rocq (formerly Coq) that are specific to this spec. It builds on the specifications and proofs of the functions of noise.c, cookie.c and lib/crypto, the protocol model, and the Diffie-Hellman agreement of X25519. The whole proof of the 38 specs is about 300,000 lines. AI agents wrote it, Rocq checks all of it, and nobody has to read it. What people have to read is the spec above, with harness/common.h, and the kernel shim that DEVIATIONS.md describes.
Other specs about receive.c: see the list on the left, or the summary.