Skip to content

net: neighbour: Notify changes atomically

Petr Machata netdev 2 versions – Applied

Status from this message: This series was applied to netdev/net-next.git (main)

Get series with b4
b4 am [email protected]

Versions newest first · linked by author and title

  1. v2 Latest8 patches 2 repliesApplied 8/8
  2. v18 patches 5 repliesSuperseded

Changes in v2 from the cover letter

- Patch #2:
- Drop the __acquires / __releases annotations at neigh_notify(). They are not necessary with a symmetrically locking function.
- Retain the R-b tag for this change.
- Patch #8:
- Do not skip the notification from inside the atomic_read(&neigh->probes) >= neigh_max_probes(neigh) conditional. Instead set a flag, and goto out after the notification if the flag is set.
- Move the __neigh_notify() call another block up above the NUD_IN_TIMER check. That belongs logically together with the (NUD_INCOMPLETE | NUD_PROBE) check afterwards, no sense to split the two conditionals with the notifier.

Patches in v2 All 8 applied

#Subject and filesSizeRepliesApplied
1/8net: core: neighbour: Add a neigh_fill_info() helper for when lock not heldnet/core/neighbour.c+17 -70 repliesApplied
2/8net: core: neighbour: Call __neigh_notify() under a locknet/core/neighbour.c+12 -60 repliesApplied
3/8net: core: neighbour: Extract ARP queue processing to a helper functionnet/core/neighbour.c+43 -350 repliesApplied
4/8net: core: neighbour: Process ARP queue laternet/core/neighbour.c+7 -10 repliesApplied
5/8net: core: neighbour: Inline neigh_update_notify() callsnet/core/neighbour.c+10 -110 repliesApplied
6/8net: core: neighbour: Reorder netlink & internal notificationnet/core/neighbour.c+2 -21 replyApplied
7/8net: core: neighbour: Make one netlink notification atomicallynet/core/neighbour.c+5 -30 repliesApplied
8/8net: core: neighbour: Make another netlink notification atomicallynet/core/neighbour.c+9 -40 repliesApplied

Reviews trailers per patch

All 8 patches have a Reviewed-by or Acked-by of their own; the series has one for all of them.

PatchIdo SchimmelSimon Horman
SeriesR-b
1/8R-b
2/8R-b
3/8R-b
4/8R-b
5/8R-b
6/8R-b
7/8R-b
8/8R-b

Trailers given in replies are filled; those carried in the patches from earlier versions are outlined with dashes.

Read the v2 thread

Cover letter [PATCH net-next v2 0/8] net: neighbour: Notify changes atomically

Andy Roulin and Francesco Ruggeri have apparently independently both hit an issue with the current neighbor notification scheme. Francesco reported the issue in [1]. In a response[2] to that report, Andy said:

    neigh_update sends a rtnl notification if an update, e.g.,
    nud_state change, was done but there is no guarantee of
    ordering of the rtnl notifications. Consider the following
    scenario:

    userspace thread                   kernel thread
    ================                   =============
    neigh_update
       write_lock_bh(n->lock)
       n->nud_state = STALE
       write_unlock_bh(n->lock)
       neigh_notify
         neigh_fill_info
           read_lock_bh(n->lock)
           ndm->nud_state = STALE
           read_unlock_bh(n->lock)
         -------------------------->
                                      neigh:update
                                      write_lock_bh(n->lock)
                                      n->nud_state = REACHABLE
                                      write_unlock_bh(n->lock)
                                      neigh_notify
                                        neigh_fill_info
                                           read_lock_bh(n->lock)
                                           ndm->nud_state = REACHABLE
                                           read_unlock_bh(n->lock)
                                        rtnl_nofify
                                      RTNL REACHABLE sent
                            <--------
        rtnl_notify
        RTNL STALE sent

    In this scenario, the kernel neigh is updated first to STALE and
    then REACHABLE but the netlink notifications are sent out of order,
    first REACHABLE and then STALE.

The solution presented in [2] was to extend the critical region to include both the call to neigh_fill_info(), as well as rtnl_notify(). Then we have a guarantee that whatever state was captured by neigh_fill_info(), will be sent right away. The above scenario can thus not happen.

This is how this patchset begins: patches #1 and #2 add helper duals to neigh_fill_info() and __neigh_notify() such that the __-prefixed function assumes the neighbor lock is held, and the unprefixed one is a thin wrapper that manages locking. This extends locking further than Andy's patch, but makes for a clear code and supports the following part.

At that point, the original race is gone. But what can happen is the following race, where the notification does not reflect the change that was made:

    userspace thread		       kernel thread
    ================		       =============
    neigh_update
       write_lock_bh(n->lock)
       n->nud_state = STALE
       write_unlock_bh(n->lock)
	 -------------------------->
				      neigh:update
				      write_lock_bh(n->lock)
				      n->nud_state = REACHABLE
				      write_unlock_bh(n->lock)
				      neigh_notify
					read_lock_bh(n->lock)
					__neigh_fill_info
					   ndm->nud_state = REACHABLE
					rtnl_notify
					read_unlock_bh(n->lock)
				      RTNL REACHABLE sent
			    <--------
       neigh_notify
	 read_lock_bh(n->lock)
	 __neigh_fill_info
	   ndm->nud_state = REACHABLE
	 rtnl_notify
	 read_unlock_bh(n->lock)
       RTNL REACHABLE sent again

Here, even though neigh_update() made a change to STALE, it later sends a notification with a NUD of REACHABLE. The obvious solution to fix this race is to move the notifier to the same critical section that actually makes the change.

Sending a notification in fact involves two things: invoking the internal notifier chain, and sending the netlink notification. The overall approach in this patchset is to move the netlink notification to the critical section of the change, while keeping the internal notifier intact. Since the motion is not obviously correct, the patchset presents the change in series of incremental steps with discussion in commit messages. Please see details in the patches themselves.

Reproducer
==========

To consistently reproduce, I injected an mdelay before the rtnl_notify() call. Since only one thread should delay, a bit of instrumentation was needed to see where the call originates. The mdelay was then only issued on the call stack rooted in the RTNL request.

Then the general idea is to issue an "ip neigh replace" to mark a neighbor entry as failed. In parallel to that, inject an ARP burst that validates the entry. This is all observed with an "ip monitor neigh", where one can see either a REACHABLE->FAILED transition, or FAILED->REACHABLE, while the actual state at the end of the sequence is always REACHABLE.

With the patchset, only FAILED->REACHABLE is ever observed in the monitor.

Alternatives
============

Another approach to solving the issue would be to have a per-neighbor queue of notification digests, each with a set of fields necessary for formatting a notification. In pseudocode, a neighbor update would look something like this:

neighbor_update:
- lock
- do update
- allocate notification digest, fill partially, mark not-committed
- unlock
- critical-section-breaking stuff (probes, ARP Q, etc.)
- lock
- fill in missing details to the digest (notably neigh->probes)
- mark the digest as committed
- while (front of the digest queue is committed)
- pop it, convert to notifier, send the notification
- unlock

This adds more complexity and would imply more changes to the code, which is why I think the approach presented in this patchset is better. But it would allow us to retain the overall structure of the code while giving us accurate notifications.

A third approach would be to consider the second race not very serious and be OK with seeing a notification that does not reflect the change that prompted it. Then a two-patch prefix of this patchset would be all that is needed.

[1]: https://lore.kernel.org/netdev/[email protected]/
[2]: https://lore.kernel.org/netdev/[email protected]/

v2:
- Patch #2:
- Drop the __acquires / __releases annotations at neigh_notify(). They are not necessary with a symmetrically locking function.
- Retain the R-b tag for this change.
- Patch #8:
- Do not skip the notification from inside the atomic_read(&neigh->probes) >= neigh_max_probes(neigh) conditional. Instead set a flag, and goto out after the notification if the flag is set.
- Move the __neigh_notify() call another block up above the NUD_IN_TIMER check. That belongs logically together with the (NUD_INCOMPLETE | NUD_PROBE) check afterwards, no sense to split the two conditionals with the notifier.

Petr Machata (8):
net: core: neighbour: Add a neigh_fill_info() helper for when lock not
held
net: core: neighbour: Call __neigh_notify() under a lock
net: core: neighbour: Extract ARP queue processing to a helper
function
net: core: neighbour: Process ARP queue later
net: core: neighbour: Inline neigh_update_notify() calls
net: core: neighbour: Reorder netlink & internal notification
net: core: neighbour: Make one netlink notification atomically
net: core: neighbour: Make another netlink notification atomically

 net/core/neighbour.c | 150 +++++++++++++++++++++++++++----------------
 1 file changed, 93 insertions(+), 57 deletions(-)
Signature · 1 line
2.51.1