Files
visor/pkg/tcpip/stack/neighbor_entry.go
Tamir DubersteinandHanzo Dev 9bf3bd12f4 checklocks: enforce neighbor state ownership
Neighbor completion schedules dequeue while holding the entry mutex.
Since 29794f8e19, the callback reads the link address at execution time
without that mutex. Use the existing completion result captured under
the lock, keeping dequeue asynchronous to preserve queue/neighbor lock
ordering.

Restore the missing cache and entry guards from #7058 with the current
lockdep mutexes. Take read-locked snapshots in existing cache assertions
so the same ownership contract is enforced in tests.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 12:23:03 -04:00

660 lines
20 KiB
Go

// Copyright 2020 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stack
import (
"fmt"
"time"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
const (
// immediateDuration is a duration of zero for scheduling work that needs to
// be done immediately but asynchronously to avoid deadlock.
immediateDuration time.Duration = 0
)
// NeighborEntry describes a neighboring device in the local network.
//
// +stateify savable
type NeighborEntry struct {
Addr tcpip.Address
LinkAddr tcpip.LinkAddress
State NeighborState
UpdatedAt tcpip.MonotonicTime
}
// NeighborState defines the state of a NeighborEntry within the Neighbor
// Unreachability Detection state machine, as per RFC 4861 section 7.3.2 and
// RFC 7048.
type NeighborState uint8
const (
// Unknown means reachability has not been verified yet. This is the initial
// state of entries that have been created automatically by the Neighbor
// Unreachability Detection state machine.
Unknown NeighborState = iota
// Incomplete means that there is an outstanding request to resolve the
// address.
Incomplete
// Reachable means the path to the neighbor is functioning properly for both
// receive and transmit paths.
Reachable
// Stale means reachability to the neighbor is unknown, but packets are still
// able to be transmitted to the possibly stale link address.
Stale
// Delay means reachability to the neighbor is unknown and pending
// confirmation from an upper-level protocol like TCP, but packets are still
// able to be transmitted to the possibly stale link address.
Delay
// Probe means a reachability confirmation is actively being sought by
// periodically retransmitting reachability probes until a reachability
// confirmation is received, or until the maximum number of probes has been
// sent.
Probe
// Static describes entries that have been explicitly added by the user. They
// do not expire and are not deleted until explicitly removed.
Static
// Unreachable means reachability confirmation failed; the maximum number of
// reachability probes has been sent and no replies have been received.
//
// TODO(gvisor.dev/issue/5472): Add the following sentence when we implement
// RFC 7048: "Packets continue to be sent to the neighbor while
// re-attempting to resolve the address."
Unreachable
)
// +stateify savable
type timer struct {
// done indicates to the timer that the timer was stopped.
done *bool
timer tcpip.Timer `state:"nosave"`
}
// +stateify savable
type neighborEntryMu struct {
neighborEntryRWMutex `state:"nosave"`
// +checklocks:neighborEntryRWMutex
neigh NeighborEntry
// done is closed when address resolution is complete. It is nil iff s is
// incomplete and resolution is not yet in progress.
// +checklocks:neighborEntryRWMutex
done chan struct{} `state:"nosave"`
// onResolve is called with the result of address resolution.
// +checklocks:neighborEntryRWMutex
onResolve []func(LinkResolutionResult) `state:"nosave"`
// +checklocks:neighborEntryRWMutex
isRouter bool
// +checklocks:neighborEntryRWMutex
timer timer
}
// neighborEntry implements a neighbor entry's individual node behavior, as per
// RFC 4861 section 7.3.3. Neighbor Unreachability Detection operates in
// parallel with the sending of packets to a neighbor, necessitating the
// entry's lock to be acquired for all operations.
//
// +stateify savable
type neighborEntry struct {
neighborEntryEntry
cache *neighborCache
// nudState points to the Neighbor Unreachability Detection configuration.
nudState *NUDState
mu neighborEntryMu
}
// newNeighborEntry creates a neighbor cache entry starting at the default
// state, Unknown. Transition out of Unknown by calling either
// `handlePacketQueuedLocked` or `handleProbeLocked` on the newly created
// neighborEntry.
func newNeighborEntry(cache *neighborCache, remoteAddr tcpip.Address, nudState *NUDState) *neighborEntry {
n := &neighborEntry{
cache: cache,
nudState: nudState,
}
n.mu.Lock()
n.mu.neigh = NeighborEntry{
Addr: remoteAddr,
State: Unknown,
}
n.mu.Unlock()
return n
}
// newStaticNeighborEntry creates a neighbor cache entry starting at the
// Static state. The entry can only transition out of Static by directly
// calling `setStateLocked`.
func newStaticNeighborEntry(cache *neighborCache, addr tcpip.Address, linkAddr tcpip.LinkAddress, state *NUDState) *neighborEntry {
entry := NeighborEntry{
Addr: addr,
LinkAddr: linkAddr,
State: Static,
UpdatedAt: cache.nic.stack.clock.NowMonotonic(),
}
n := &neighborEntry{
cache: cache,
nudState: state,
}
n.mu.Lock()
n.mu.neigh = entry
n.mu.Unlock()
return n
}
// notifyCompletionLocked notifies those waiting for address resolution, with
// the link address if resolution completed successfully.
//
// +checklocks:e.mu.neighborEntryRWMutex
func (e *neighborEntry) notifyCompletionLocked(err tcpip.Error) {
res := LinkResolutionResult{LinkAddress: e.mu.neigh.LinkAddr, Err: err}
for _, callback := range e.mu.onResolve {
callback(res)
}
e.mu.onResolve = nil
if ch := e.mu.done; ch != nil {
close(ch)
e.mu.done = nil
// Dequeue the pending packets asynchronously to not hold up the current
// goroutine as writing packets may be a costly operation.
//
// At the time of writing, when writing packets, a neighbor's link address
// is resolved (which ends up obtaining the entry's lock) while holding the
// link resolution queue's lock. Dequeuing packets asynchronously avoids a
// lock ordering violation.
//
// NB: this is equivalent to spawning a goroutine directly using the go
// keyword but allows tests that use manual clocks to deterministically
// wait for this work to complete.
e.cache.nic.stack.clock.AfterFunc(0, func() {
e.cache.nic.linkResQueue.dequeue(ch, res.LinkAddress, err)
})
}
}
// dispatchAddEventLocked signals to stack's NUD Dispatcher that the entry has
// been added.
//
// +checklocks:e.mu.neighborEntryRWMutex
func (e *neighborEntry) dispatchAddEventLocked() {
if nudDisp := e.cache.nic.stack.nudDisp; nudDisp != nil {
nudDisp.OnNeighborAdded(e.cache.nic.id, e.mu.neigh)
}
}
// dispatchChangeEventLocked signals to stack's NUD Dispatcher that the entry
// has changed state or link-layer address.
//
// +checklocks:e.mu.neighborEntryRWMutex
func (e *neighborEntry) dispatchChangeEventLocked() {
if nudDisp := e.cache.nic.stack.nudDisp; nudDisp != nil {
nudDisp.OnNeighborChanged(e.cache.nic.id, e.mu.neigh)
}
}
// dispatchRemoveEventLocked signals to stack's NUD Dispatcher that the entry
// has been removed.
//
// +checklocks:e.mu.neighborEntryRWMutex
func (e *neighborEntry) dispatchRemoveEventLocked() {
if nudDisp := e.cache.nic.stack.nudDisp; nudDisp != nil {
nudDisp.OnNeighborRemoved(e.cache.nic.id, e.mu.neigh)
}
}
// cancelTimerLocked cancels the currently scheduled action, if there is one.
// Entries in Unknown, Stale, or Static state do not have a scheduled action.
//
// +checklocks:e.mu.neighborEntryRWMutex
func (e *neighborEntry) cancelTimerLocked() {
if e.mu.timer.timer != nil {
e.mu.timer.timer.Stop()
*e.mu.timer.done = true
e.mu.timer = timer{}
}
}
// removeLocked prepares the entry for removal.
//
// +checklocks:e.mu.neighborEntryRWMutex
func (e *neighborEntry) removeLocked() {
e.mu.neigh.UpdatedAt = e.cache.nic.stack.clock.NowMonotonic()
e.dispatchRemoveEventLocked()
// Set state to unknown to invalidate this entry if it's cached in a Route.
e.setStateLocked(Unknown)
e.cancelTimerLocked()
// TODO(https://gvisor.dev/issues/5583): test the case where this function is
// called during resolution; that can happen in at least these scenarios:
//
// - manual address removal during resolution
//
// - neighbor cache eviction during resolution
e.notifyCompletionLocked(&tcpip.ErrAborted{})
}
// setStateLocked transitions the entry to the specified state immediately.
//
// Follows the logic defined in RFC 4861 section 7.3.3.
//
// +checklocks:e.mu.neighborEntryRWMutex
func (e *neighborEntry) setStateLocked(next NeighborState) {
e.cancelTimerLocked()
prev := e.mu.neigh.State
e.mu.neigh.State = next
e.mu.neigh.UpdatedAt = e.cache.nic.stack.clock.NowMonotonic()
config := e.nudState.Config()
switch next {
case Incomplete:
panic(fmt.Sprintf("should never transition to Incomplete with setStateLocked; neigh = %#v, prev state = %s", e.mu.neigh, prev))
case Reachable:
// Protected by e.mu.
done := false
e.mu.timer = timer{
done: &done,
timer: e.cache.nic.stack.Clock().AfterFunc(e.nudState.ReachableTime(), func() {
e.mu.Lock()
defer e.mu.Unlock()
if done {
// The timer was stopped because the entry changed state.
return
}
e.setStateLocked(Stale)
e.dispatchChangeEventLocked()
}),
}
case Delay:
// Protected by e.mu.
done := false
e.mu.timer = timer{
done: &done,
timer: e.cache.nic.stack.Clock().AfterFunc(config.DelayFirstProbeTime, func() {
e.mu.Lock()
defer e.mu.Unlock()
if done {
// The timer was stopped because the entry changed state.
return
}
e.setStateLocked(Probe)
e.dispatchChangeEventLocked()
}),
}
case Probe:
// Protected by e.mu.
done := false
remaining := config.MaxUnicastProbes
addr := e.mu.neigh.Addr
linkAddr := e.mu.neigh.LinkAddr
// Send a probe in another gorountine to free this thread of execution
// for finishing the state transition. This is necessary to escape the
// currently held lock so we can send the probe message without holding
// a shared lock.
e.mu.timer = timer{
done: &done,
timer: e.cache.nic.stack.Clock().AfterFunc(immediateDuration, func() {
var err tcpip.Error = &tcpip.ErrTimeout{}
if remaining != 0 {
err = e.cache.linkRes.LinkAddressRequest(addr, tcpip.Address{} /* localAddr */, linkAddr)
}
e.mu.Lock()
defer e.mu.Unlock()
if done {
// The timer was stopped because the entry changed state.
return
}
if err != nil {
e.setStateLocked(Unreachable)
e.notifyCompletionLocked(err)
e.dispatchChangeEventLocked()
return
}
remaining--
e.mu.timer.timer.Reset(config.RetransmitTimer)
}),
}
case Unreachable:
case Unknown, Stale, Static:
// Do nothing
default:
panic(fmt.Sprintf("Invalid state transition from %q to %q", prev, next))
}
}
// handlePacketQueuedLocked advances the state machine according to a packet
// being queued for outgoing transmission.
//
// Follows the logic defined in RFC 4861 section 7.3.3.
//
// +checklocks:e.mu.neighborEntryRWMutex
func (e *neighborEntry) handlePacketQueuedLocked(localAddr tcpip.Address) {
switch e.mu.neigh.State {
case Unknown, Unreachable:
prev := e.mu.neigh.State
e.mu.neigh.State = Incomplete
e.mu.neigh.UpdatedAt = e.cache.nic.stack.clock.NowMonotonic()
switch prev {
case Unknown:
e.dispatchAddEventLocked()
case Unreachable:
e.dispatchChangeEventLocked()
e.cache.nic.stats.neighbor.unreachableEntryLookups.Increment()
}
config := e.nudState.Config()
// Protected by e.mu.
done := false
remaining := config.MaxMulticastProbes
addr := e.mu.neigh.Addr
// Send a probe in another gorountine to free this thread of execution
// for finishing the state transition. This is necessary to escape the
// currently held lock so we can send the probe message without holding
// a shared lock.
e.mu.timer = timer{
done: &done,
timer: e.cache.nic.stack.Clock().AfterFunc(immediateDuration, func() {
var err tcpip.Error = &tcpip.ErrTimeout{}
if remaining != 0 {
// As per RFC 4861 section 7.2.2:
//
// If the source address of the packet prompting the solicitation is
// the same as one of the addresses assigned to the outgoing interface,
// that address SHOULD be placed in the IP Source Address of the
// outgoing solicitation.
//
err = e.cache.linkRes.LinkAddressRequest(addr, localAddr, "" /* linkAddr */)
}
e.mu.Lock()
defer e.mu.Unlock()
if done {
// The timer was stopped because the entry changed state.
return
}
if err != nil {
e.setStateLocked(Unreachable)
e.notifyCompletionLocked(err)
e.dispatchChangeEventLocked()
return
}
remaining--
e.mu.timer.timer.Reset(config.RetransmitTimer)
}),
}
case Stale:
e.setStateLocked(Delay)
e.dispatchChangeEventLocked()
case Incomplete, Reachable, Delay, Probe, Static:
// Do nothing
default:
panic(fmt.Sprintf("Invalid cache entry state: %s", e.mu.neigh.State))
}
}
// handleProbeLocked processes an incoming neighbor probe (e.g. ARP request or
// Neighbor Solicitation for ARP or NDP, respectively).
//
// Follows the logic defined in RFC 4861 section 7.2.3.
//
// +checklocks:e.mu.neighborEntryRWMutex
func (e *neighborEntry) handleProbeLocked(remoteLinkAddr tcpip.LinkAddress) {
// Probes MUST be silently discarded if the target address is tentative, does
// not exist, or not bound to the NIC as per RFC 4861 section 7.2.3. These
// checks MUST be done by the NetworkEndpoint.
switch e.mu.neigh.State {
case Unknown:
e.mu.neigh.LinkAddr = remoteLinkAddr
e.setStateLocked(Stale)
e.dispatchAddEventLocked()
case Incomplete:
// "If an entry already exists, and the cached link-layer address
// differs from the one in the received Source Link-Layer option, the
// cached address should be replaced by the received address, and the
// entry's reachability state MUST be set to STALE."
// - RFC 4861 section 7.2.3
e.mu.neigh.LinkAddr = remoteLinkAddr
e.setStateLocked(Stale)
e.notifyCompletionLocked(nil)
e.dispatchChangeEventLocked()
case Reachable, Delay, Probe:
if e.mu.neigh.LinkAddr != remoteLinkAddr {
e.mu.neigh.LinkAddr = remoteLinkAddr
e.setStateLocked(Stale)
e.dispatchChangeEventLocked()
}
case Stale:
if e.mu.neigh.LinkAddr != remoteLinkAddr {
e.mu.neigh.LinkAddr = remoteLinkAddr
e.dispatchChangeEventLocked()
}
case Unreachable:
// TODO(gvisor.dev/issue/5472): Do not change the entry if the link
// address is the same, as per RFC 7048.
e.mu.neigh.LinkAddr = remoteLinkAddr
e.setStateLocked(Stale)
e.dispatchChangeEventLocked()
case Static:
// Do nothing
default:
panic(fmt.Sprintf("Invalid cache entry state: %s", e.mu.neigh.State))
}
}
// handleConfirmationLocked processes an incoming neighbor confirmation
// (e.g. ARP reply or Neighbor Advertisement for ARP or NDP, respectively).
//
// Follows the state machine defined by RFC 4861 section 7.2.5.
//
// TODO(gvisor.dev/issue/2277): To protect against ARP poisoning and other
// attacks against NDP functions, Secure Neighbor Discovery (SEND) Protocol
// should be deployed where preventing access to the broadcast segment might
// not be possible. SEND uses RSA key pairs to produce Cryptographically
// Generated Addresses (CGA), as defined in RFC 3972. This ensures that the
// claimed source of an NDP message is the owner of the claimed address.
//
// +checklocks:e.mu.neighborEntryRWMutex
func (e *neighborEntry) handleConfirmationLocked(linkAddr tcpip.LinkAddress, flags ReachabilityConfirmationFlags) {
switch e.mu.neigh.State {
case Incomplete:
if len(linkAddr) == 0 {
// "If the link layer has addresses and no Target Link-Layer Address
// option is included, the receiving node SHOULD silently discard the
// received advertisement." - RFC 4861 section 7.2.5
e.cache.nic.stats.neighbor.droppedInvalidLinkAddressConfirmations.Increment()
break
}
e.mu.neigh.LinkAddr = linkAddr
if flags.Solicited {
e.setStateLocked(Reachable)
} else {
e.setStateLocked(Stale)
}
e.dispatchChangeEventLocked()
e.mu.isRouter = flags.IsRouter
e.notifyCompletionLocked(nil)
// "Note that the Override flag is ignored if the entry is in the
// INCOMPLETE state." - RFC 4861 section 7.2.5
case Reachable, Stale, Delay, Probe:
isLinkAddrDifferent := len(linkAddr) != 0 && e.mu.neigh.LinkAddr != linkAddr
if isLinkAddrDifferent {
if !flags.Override {
if e.mu.neigh.State == Reachable {
e.setStateLocked(Stale)
e.dispatchChangeEventLocked()
}
break
}
e.mu.neigh.LinkAddr = linkAddr
if !flags.Solicited {
if e.mu.neigh.State != Stale {
e.setStateLocked(Stale)
e.dispatchChangeEventLocked()
} else {
// Notify the LinkAddr change, even though NUD state hasn't changed.
e.dispatchChangeEventLocked()
}
break
}
}
if flags.Solicited && (flags.Override || !isLinkAddrDifferent) {
wasReachable := e.mu.neigh.State == Reachable
// Set state to Reachable again to refresh timers.
e.setStateLocked(Reachable)
e.notifyCompletionLocked(nil)
if !wasReachable {
e.dispatchChangeEventLocked()
}
}
if e.mu.isRouter && !flags.IsRouter && header.IsV6UnicastAddress(e.mu.neigh.Addr) {
// "In those cases where the IsRouter flag changes from TRUE to FALSE as
// a result of this update, the node MUST remove that router from the
// Default Router List and update the Destination Cache entries for all
// destinations using that neighbor as a router as specified in Section
// 7.3.3. This is needed to detect when a node that is used as a router
// stops forwarding packets due to being configured as a host."
// - RFC 4861 section 7.2.5
//
// TODO(gvisor.dev/issue/4085): Remove the special casing we do for IPv6
// here.
ep := e.cache.nic.getNetworkEndpoint(header.IPv6ProtocolNumber)
if ep == nil {
panic("have a neighbor entry for an IPv6 router but no IPv6 network endpoint")
}
if ndpEP, ok := ep.(NDPEndpoint); ok {
ndpEP.InvalidateDefaultRouter(e.mu.neigh.Addr)
}
}
e.mu.isRouter = flags.IsRouter
case Unknown, Unreachable, Static:
// Do nothing
default:
panic(fmt.Sprintf("Invalid cache entry state: %s", e.mu.neigh.State))
}
}
// handleUpperLevelConfirmation processes an incoming upper-level protocol
// (e.g. TCP acknowledgements) reachability confirmation.
func (e *neighborEntry) handleUpperLevelConfirmation() {
tryHandleConfirmation := func() bool {
switch e.mu.neigh.State {
case Stale, Delay, Probe:
return true
case Reachable:
// Avoid setStateLocked; Timer.Reset is cheaper.
//
// Note that setting the timer does not need to be protected by the
// entry's write lock since we do not modify the timer pointer, but the
// time the timer should fire. The timer should have internal locks to
// synchronize timer resets changes with the clock.
e.mu.timer.timer.Reset(e.nudState.ReachableTime())
return false
case Unknown, Incomplete, Unreachable, Static:
// Do nothing
return false
default:
panic(fmt.Sprintf("Invalid cache entry state: %s", e.mu.neigh.State))
}
}
e.mu.RLock()
needsTransition := tryHandleConfirmation()
e.mu.RUnlock()
if !needsTransition {
return
}
// We need to transition the neighbor to Reachable so take the write lock and
// perform the transition, but only if we still need the transition since the
// state could have changed since we dropped the read lock above.
e.mu.Lock()
defer e.mu.Unlock()
if needsTransition := tryHandleConfirmation(); needsTransition {
e.setStateLocked(Reachable)
e.dispatchChangeEventLocked()
}
}
// getRemoteLinkAddress returns the entry's link address and whether that link
// address is valid.
func (e *neighborEntry) getRemoteLinkAddress() (tcpip.LinkAddress, bool) {
e.mu.RLock()
defer e.mu.RUnlock()
switch e.mu.neigh.State {
case Reachable, Static, Delay, Probe:
return e.mu.neigh.LinkAddr, true
case Unknown, Incomplete, Unreachable, Stale:
return "", false
default:
panic(fmt.Sprintf("invalid state for neighbor entry %v: %v", e.mu.neigh, e.mu.neigh.State))
}
}