Memory barriers
A program's loads and stores do not necessarily reach memory in the order the source lists them. The compiler may reorder accesses it believes independent, and the CPU may make a later store visible before an earlier one, or perform a later load before an earlier one, as long as the running thread cannot tell. A device can tell: it reads and writes DMA memory on its own, and sees only the order in which the CPU's stores became visible.
Handing over a descriptor shows the problem. The driver fills in an address and a length, then sets the flag that gives the descriptor to the device; if the flag becomes visible first, the device acts on the old length. Completion is the mirror case: the driver reads a flag the device set, then the data the flag says is ready, and a CPU that loaded the data early returns what was there before the device wrote. A write barrier between the descriptor and the flag makes every earlier store visible before any later one. A read barrier between the flag and the data keeps later loads from running ahead of it. Both also stop the compiler from moving accesses across them.
The trap is that x64 keeps stores in order with stores, and loads with loads, by itself, so a missing barrier does no harm there. It shows up on ARM64, whose memory model lets both be reordered, and a driver tested on x64 alone can carry the bug for a long time.
In the driver kit, DmaBuffer.WriteBarrier() and DmaBuffer.ReadBarrier() are the two barriers: real fences on ARM64, and on x64 no instruction, though they still keep the compiler from moving accesses across them. Every RegisterWindow write is preceded by a write barrier and every read followed by a read barrier, so the doorbell write after a filled descriptor needs none of its own, while a DeviceRegion carries none and the driver places them itself. See DMA memory and Register windows.