mirror of
https://github.com/amnezia-vpn/amneziawg-go.git
synced 2026-10-14 05:41:54 +00:00
[GH-ISSUE #204] device: data race on Timer.duration between timer expiry and Mod/IsPending #94
Labels
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
amnezia-vpn/amneziawg-go#94
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @StarProxima on GitHub (Oct 9, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amneziawg-go/issues/204
Version: v3.1.20260828 (
masteratb5928ef). The code comes from457d920("feat: amneziawg 3.0") and is in every tag since v3.0.0.What happens
The expiry function of a
Timerreadstimer.durationundermodifyingLock, releases the lock and only then clears the field (device/timers.go#L34-L41):Mod,DelandIsPendingaccess the same field undermodifyingLock(L49-L74). Nothing orders the write on line 41 with them, which has two consequences:go test -racecan hit it; we did once in the tests of an Xray-core integration, with the write on line 41 in the report.Modthat runs between the unlock and the write loses its duration. The timer is armed again, butdurationis back to 0, so when it fires, the expiry function returns early without calling the handler, andIsPendingreports false until then. On 32-bit platforms the unsynchronizedint64access can also tear. The window is a few instructions wide and most handlers re-arm their own timer, so I have not seen a visible effect beyond the race reports.Before 3.0 the timer cleared its pending state before unlocking, as wireguard-go still does with
isPending(wireguard-go device/timers.go).Reproduction
Add to
device/:The loop waits in
IsPendinguntil the expiry clears the duration, so the read always follows the unlock on line 40 and the race detector sees it every time: 100 failures in 100 separate runs (macOS arm64, Go 1.26.4). Separate processes, because the detector reports a given race only once per process.Expected
No race: the expiry clears the duration while it still holds
modifyingLock, and aModthat runs after the expiry read the duration keeps its value.#194 includes a fix for this in its last commit ("device: fix race between timer expiry and reset", which restores a separate
isPendingflag), together with four unrelated backports from wireguard-go. The test above passes on the head of #194.