Three rigs, on every pull request, against machines of their own. PowerShell and shell scripts — the only binary involved is the driver you are testing.
Five tunnels, both directions, every pull request.
Malformed input OpenVPN never sends; orderings it can never produce.
Twenty thousand peer sessions a run, keys rotating throughout.
-Win11 driver build. Server 2022 machines are kept, stopped, as the way back..sys; the machines carry no toolchain. Stress and ioctl take a checked build with
Verifier armed, perf a release one — a checked build traces the data path and reads a
factor low.Every ioctl, called with mutated buffers — in multipeer mode, then again point-to-point, where the multipeer half should be refused — plus control codes the driver does not implement. Three things get mutated:
Input truncated to one byte and to one short of the struct, oversized, absent. Output too small, absent. Two past findings lived here, so it runs against every ioctl.
Peer ids at the ends of the range, key lengths that are not 16, 24 or 32, enums past their
last member, prefix lengths past 32 and 128, and the sockaddr families that
decide how far the driver reads.
Null with a non-zero length, and lengths that would overflow a size calculation.
Nothing asserts on the status a call returns. A refusal is the right answer to most of this and the driver may choose which refusal, so the run only calls out what should never have been accepted at all — a buffer shorter than the struct the driver reads, or a value outside its documented range. Driver Verifier judges the rest.
Ids come from a deliberately small space, so they are reused constantly, and one thread installs keys on the ids another is deleting.
On the control channel a reader finds the queue empty and parks, while the receive side takes the same lock to decide whether to hand it a packet or queue one. That window is the bug. So control traffic is deliberately rare — one datagram in 256. Flood it and the queue is never empty, the reader always finds a packet waiting, never parks, and never races.
A peer's own address is in the peer table; subnets reachable behind a peer are in a trie, consulted only when that table misses. Peer churn rewrites the trie thousands of times a run, and traffic between tunnel addresses never once reads it — so the probes have to manufacture the lookups.
Every client is handed the same iroute, 10.90.0.0/24, so the prefix always has an
owner and ownership moves every time anyone connects. Addressing that subnet rather than a tunnel
address is what pushes a lookup past the peer table. It happens on two paths:
send path · destination lookup
A destination no peer holds as its own address. The trie resolves the prefix to its owner, which does hold it and answers. The reply proves the trie routed out.
receive path · source lookup
Reverse-path filtering checks the source against the sending peer, misses, falls to the trie. Only the real owner is answered, so a reply proves the trie validated a source coming in — and names the owner.
send path again · 100/s · all run
No table can ever answer it, so every packet reaches the trie — and nothing replies, because
the owner is usually a dev null client. It asserts nothing. It re-runs probe 1
while churn rewrites the structure underneath.
Probes 1 and 2 run before the swarm and the flood begin, and that ordering matters: later the owner is usually a flood client with no interface at all, which would resolve and deliver correctly and still never answer.
Needs a control write addressed to the driver's own endpoint while another writer is queued. An ordering OpenVPN never produces.
The window is microseconds. The swarm kills clients with pings still in flight — exactly when the driver frees a peer and detaches its timer.
A reader parks, a packet arrives, both take the same lock. It needs a reader that actually has to park — a flood would just leave a backlog and race nothing.
Correctness tests pass: the handshake completes. Only the clock shows it.
CNG's ChaCha20-Poly1305 is scalar and bound per byte. On one core AES-256-GCM runs 25× faster, because AES-NI does it in hardware. That is fine while AES is the default — but anyone who configures ChaCha pays for it.
We take the assembly from dot-asm/cryptogams, not OpenSSL: same author, same code, but the original 3-clause BSD rather than OpenSSL's Apache-2.0, which a GPLv2 driver cannot take. The cipher got 4.7× faster; the tunnel gained 34% and 21%.
Two gigabytes into a run, the box died — in unrelated code, with
ovpn_dco nowhere on the stack. poly1305's VPMADD52 path ends in
vzeroall, which zeroes ymm0–15. On Win64 the low halves of XMM6–15 are
the caller's to keep, and MSVC had live values in them.
The fix is one word — vzeroall to vzeroupper
— after which 50.6 GB went through clean at 7.24 Gbit/s with the fast path still live.
Nothing cheaper would have found it.
OpenSSL knew: #12744 reported exactly this in 2020, #21522 the same for the 8x path, and #21808 fixed both by excluding VPMADD52 on Win64 outright. dot-asm's tree — the BSD-licensed one a GPLv2 driver can actually use — still ships it unguarded.
tests/perf · tests/ioctl · tests/stress — each with a
README covering setup and what every counter means.