neigh: Really delete an arp/neigh entry on "ip neigh delete" or "arp -d"
authorSowmini Varadhan <sowmini.varadhan@oracle.com>
Fri, 2 Jun 2017 16:01:49 +0000 (09:01 -0700)
committerDavid S. Miller <davem@davemloft.net>
Mon, 5 Jun 2017 01:37:18 +0000 (21:37 -0400)
The command
  # arp -s 62.2.0.1 a:b:c:d:e:f dev eth2
adds an entry like the following (listed by "arp -an")
  ? (62.2.0.1) at 0a:0b:0c:0d:0e:0f [ether] PERM on eth2
but the symmetric deletion command
  # arp -i eth2 -d 62.2.0.1
does not remove the PERM entry from the table, and instead leaves behind
  ? (62.2.0.1) at <incomplete> on eth2

The reason is that there is a refcnt of 1 for the arp_tbl itself
(neigh_alloc starts off the entry with a refcnt of 1), thus
the neigh_release() call from arp_invalidate() will (at best) just
decrement the ref to 1, but will never actually free it from the
table.

To fix this, we need to do something like neigh_forced_gc: if
the refcnt is 1 (i.e., on the table's ref), remove the entry from
the table and free it. This patch refactors and shares common code
between neigh_forced_gc and the newly added neigh_remove_one.

A similar issue exists for IPv6 Neighbor Cache entries, and is fixed
in a similar manner by this patch.

Signed-off-by: Sowmini Varadhan <sowmini.varadhan@oracle.com>
Reviewed-by: Julian Anastasov <ja@ssi.bg>
Signed-off-by: David S. Miller <davem@davemloft.net>
include/net/neighbour.h
net/core/neighbour.c
net/ipv4/arp.c

index e4dd3a2140341049fabfbff1ec55406fbe11d5b2..639b67564a7d05df0ccf9aa503131543f155ef91 100644 (file)
@@ -317,6 +317,7 @@ int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb);
 int neigh_update(struct neighbour *neigh, const u8 *lladdr, u8 new, u32 flags,
                 u32 nlmsg_pid);
 void __neigh_set_probe_once(struct neighbour *neigh);
+bool neigh_remove_one(struct neighbour *ndel, struct neigh_table *tbl);
 void neigh_changeaddr(struct neigh_table *tbl, struct net_device *dev);
 int neigh_ifdown(struct neigh_table *tbl, struct net_device *dev);
 int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb);
index d274f81fcc2c08f1e85df4ed00b9a034f3ae0739..dadb5eef91c39eb192b2babdac6f1ce26df69d3b 100644 (file)
@@ -118,6 +118,50 @@ unsigned long neigh_rand_reach_time(unsigned long base)
 EXPORT_SYMBOL(neigh_rand_reach_time);
 
 
+static bool neigh_del(struct neighbour *n, __u8 state,
+                     struct neighbour __rcu **np, struct neigh_table *tbl)
+{
+       bool retval = false;
+
+       write_lock(&n->lock);
+       if (atomic_read(&n->refcnt) == 1 && !(n->nud_state & state)) {
+               struct neighbour *neigh;
+
+               neigh = rcu_dereference_protected(n->next,
+                                                 lockdep_is_held(&tbl->lock));
+               rcu_assign_pointer(*np, neigh);
+               n->dead = 1;
+               retval = true;
+       }
+       write_unlock(&n->lock);
+       if (retval)
+               neigh_cleanup_and_release(n);
+       return retval;
+}
+
+bool neigh_remove_one(struct neighbour *ndel, struct neigh_table *tbl)
+{
+       struct neigh_hash_table *nht;
+       void *pkey = ndel->primary_key;
+       u32 hash_val;
+       struct neighbour *n;
+       struct neighbour __rcu **np;
+
+       nht = rcu_dereference_protected(tbl->nht,
+                                       lockdep_is_held(&tbl->lock));
+       hash_val = tbl->hash(pkey, ndel->dev, nht->hash_rnd);
+       hash_val = hash_val >> (32 - nht->hash_shift);
+
+       np = &nht->hash_buckets[hash_val];
+       while ((n = rcu_dereference_protected(*np,
+                                             lockdep_is_held(&tbl->lock)))) {
+               if (n == ndel)
+                       return neigh_del(n, 0, np, tbl);
+               np = &n->next;
+       }
+       return false;
+}
+
 static int neigh_forced_gc(struct neigh_table *tbl)
 {
        int shrunk = 0;
@@ -140,19 +184,10 @@ static int neigh_forced_gc(struct neigh_table *tbl)
                         * - nobody refers to it.
                         * - it is not permanent
                         */
-                       write_lock(&n->lock);
-                       if (atomic_read(&n->refcnt) == 1 &&
-                           !(n->nud_state & NUD_PERMANENT)) {
-                               rcu_assign_pointer(*np,
-                                       rcu_dereference_protected(n->next,
-                                                 lockdep_is_held(&tbl->lock)));
-                               n->dead = 1;
-                               shrunk  = 1;
-                               write_unlock(&n->lock);
-                               neigh_cleanup_and_release(n);
+                       if (neigh_del(n, NUD_PERMANENT, np, tbl)) {
+                               shrunk = 1;
                                continue;
                        }
-                       write_unlock(&n->lock);
                        np = &n->next;
                }
        }
@@ -1649,7 +1684,10 @@ static int neigh_delete(struct sk_buff *skb, struct nlmsghdr *nlh,
                           NEIGH_UPDATE_F_OVERRIDE |
                           NEIGH_UPDATE_F_ADMIN,
                           NETLINK_CB(skb).portid);
+       write_lock_bh(&tbl->lock);
        neigh_release(neigh);
+       neigh_remove_one(neigh, tbl);
+       write_unlock_bh(&tbl->lock);
 
 out:
        return err;
index e9f3386a528b4a792839d5e50894a68d3097eb42..a651c53260ec91d6b038e03d849f6e6d322aa4c7 100644 (file)
@@ -1113,13 +1113,17 @@ static int arp_invalidate(struct net_device *dev, __be32 ip)
 {
        struct neighbour *neigh = neigh_lookup(&arp_tbl, &ip, dev);
        int err = -ENXIO;
+       struct neigh_table *tbl = &arp_tbl;
 
        if (neigh) {
                if (neigh->nud_state & ~NUD_NOARP)
                        err = neigh_update(neigh, NULL, NUD_FAILED,
                                           NEIGH_UPDATE_F_OVERRIDE|
                                           NEIGH_UPDATE_F_ADMIN, 0);
+               write_lock_bh(&tbl->lock);
                neigh_release(neigh);
+               neigh_remove_one(neigh, tbl);
+               write_unlock_bh(&tbl->lock);
        }
 
        return err;