Suggested title
device: first transport packet can use stale S4 after UAPI configuration
Affected upstream source
- Repository:
https://github.com/amnezia-vpn/amneziawg-go
- Version:
v3.0.3
- Immutable revision:
cf9d2dd202821301f7039093b0a1b3d4b574c47c
- At investigation time, upstream
master was identical to that revision.
- Linux test environment:
golang:1.25.12, Docker with CAP_NET_ADMIN and
/dev/net/tun.
Safe reproduction
This reproducer uses only upstream tests, localhost UDP and freshly generated
test keys. It contains no gateway address, production key, client config or
credential.
docker run --rm --cap-add=NET_ADMIN --device /dev/net/tun \
-v $PWD:/src:ro -w /src golang:1.25.12 \
go test ./device -run '^TestAWGDevicePing$' -race=false -count=10
Observed on the pinned revision: the AWG test fails after a successful
handshake with a ping/pong transit timeout. A profile bisect showed that
S4=25 is sufficient: seven of ten isolated runs failed; H1–H4 alone and
S1–S3 alone passed.
Causal evidence
Device.NewDevice starts RoutineReadFromTUN before the caller has applied
UAPI configuration. In device/send.go, that routine loads
device.paddings.transport, calculates offset, and then blocks in
tun.device.Read. If UAPI commits S4=25 while that read is waiting, the
first returned packet is still associated with the stale local S4=0.
The receiver has already applied S4 and therefore calls
DeterminePacketTypeAndPadding expecting the transport header at offset 25.
It rejects the packet before key lookup or AEAD decrypt.
Temporary instrumentation in a disposable source copy correlated one failing
run as follows:
- sender TUN reader started the blocking read with
S4=0;
- UAPI committed
S4=25 on both devices;
- sender returned the first packet with its original
S4=0 snapshot;
- receiver reported
s4=25, expected H4/type 4, observed type 4 at byte
zero and random bytes at byte 25, then logged Received message with unknown type.
This is a stale snapshot across a blocking syscall, not a key exchange,
header-protection-key, routing or NAT failure.
Candidate direction for maintainer review
The following was validated only in an ephemeral Docker copy; it is not a
proposed final patch. Immediately after Read, re-load the current transport
padding. If it changed, move every just-read payload from the original offset
to the current offset using overlap-safe copy, then use that same current
padding for the outgoing element:
count, readErr = device.tun.device.Read(bufs, sizes, offset)
currentPadding := device.paddings.transport.Load()
if currentPadding != padding {
currentOffset := MessageTransportHeaderSize + int(currentPadding)
for i := 0; i < count; i++ {
copy(bufs[i][currentOffset:currentOffset+sizes[i]],
bufs[i][offset:offset+sizes[i]])
}
padding = currentPadding
offset = currentOffset
}
With that source-copy change:
- S4-only reproduction passed 10/10 with
-race=false;
- full
TestAWGDevicePing passed 10/10 with -race=false;
- full
TestAWGDevicePing passed 10/10 with -race.
Please advise whether re-basing the packet buffer after Read is the intended
configuration-update behavior, or whether initial UAPI configuration should
instead gate the first TUN read. A final upstream patch should include a
deterministic regression test for this startup ordering.
Downstream safety posture
No production data plane has been enabled from this finding. The downstream
canary remains zero-peer with UDP closed and forwarding/NAT disabled. No local
fork or S4 workaround will be deployed before a reviewed upstream or otherwise
immutable-pinned fix, exact-artifact verification, and closed synthetic-peer
staging.
Suggested title
device: first transport packet can use stale S4 after UAPI configurationAffected upstream source
https://github.com/amnezia-vpn/amneziawg-gov3.0.3cf9d2dd202821301f7039093b0a1b3d4b574c47cmasterwas identical to that revision.golang:1.25.12, Docker withCAP_NET_ADMINand/dev/net/tun.Safe reproduction
This reproducer uses only upstream tests, localhost UDP and freshly generated
test keys. It contains no gateway address, production key, client config or
credential.
Observed on the pinned revision: the AWG test fails after a successful
handshake with a ping/pong transit timeout. A profile bisect showed that
S4=25is sufficient: seven of ten isolated runs failed; H1–H4 alone andS1–S3 alone passed.
Causal evidence
Device.NewDevicestartsRoutineReadFromTUNbefore the caller has appliedUAPI configuration. In
device/send.go, that routine loadsdevice.paddings.transport, calculatesoffset, and then blocks intun.device.Read. If UAPI commitsS4=25while that read is waiting, thefirst returned packet is still associated with the stale local
S4=0.The receiver has already applied S4 and therefore calls
DeterminePacketTypeAndPaddingexpecting the transport header at offset 25.It rejects the packet before key lookup or AEAD decrypt.
Temporary instrumentation in a disposable source copy correlated one failing
run as follows:
S4=0;S4=25on both devices;S4=0snapshot;s4=25, expected H4/type4, observed type4at bytezero and random bytes at byte 25, then logged
Received message with unknown type.This is a stale snapshot across a blocking syscall, not a key exchange,
header-protection-key, routing or NAT failure.
Candidate direction for maintainer review
The following was validated only in an ephemeral Docker copy; it is not a
proposed final patch. Immediately after
Read, re-load the current transportpadding. If it changed, move every just-read payload from the original offset
to the current offset using overlap-safe
copy, then use that same currentpadding for the outgoing element:
With that source-copy change:
-race=false;TestAWGDevicePingpassed 10/10 with-race=false;TestAWGDevicePingpassed 10/10 with-race.Please advise whether re-basing the packet buffer after
Readis the intendedconfiguration-update behavior, or whether initial UAPI configuration should
instead gate the first TUN read. A final upstream patch should include a
deterministic regression test for this startup ordering.
Downstream safety posture
No production data plane has been enabled from this finding. The downstream
canary remains zero-peer with UDP closed and forwarding/NAT disabled. No local
fork or S4 workaround will be deployed before a reviewed upstream or otherwise
immutable-pinned fix, exact-artifact verification, and closed synthetic-peer
staging.