Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider:

Critical: 0 · High: 0 · Medium: 2 · Low: 2

- [Medium] rtnl_dump_mcaddr()/rtnl_dump_mcaddr_dev()
  (net/core/rtnetlink.c) resume a truncated dump purely positionally —…
- [Medium] rtnl_fill_mcaddr() (net/core/rtnetlink.c) copies dev->addr_len
  bytes out of ha->addr[MAX_ADDR_LEN] into the IFA_MULTICAST attribute:…
- [Low] Claimed uAPI/YAML divergence: Documentation/netlink/specs/rt-
  addr.yaml documents op `getmulticast` as 'Get / dump IPv4/IPv6…
- [Low] Claimed spec/implementation mismatch: rt-addr.yaml lists
  `cacheinfo` in the getmulticast reply attribute set (mcaddr-attrs) and…

--

rtnetlink: add AF_PACKET multicast dumps

This adds a PF_PACKET RTM_GETMULTICAST dump handler that walks dev->mc for
every device in the netns and reports each netdev_hw_addr in the existing
ifaddrmsg format with IFA_MULTICAST and IFA_MC_USERS.  The dump runs under
RCU plus netif_addr_lock_bh() and is registered with
RTNL_FLAG_DUMP_UNLOCKED.

A few questions below.

> diff --git a/net/core/rtnetlink.c b/net/core/rtnetlink.c
> index 81c5a6104dea1..5e83232c15048 100644
> --- a/net/core/rtnetlink.c
> +++ b/net/core/rtnetlink.c
> @@ -4566,6 +4566,136 @@ static int rtnl_dump_all(struct sk_buff *skb, struct 
> netlink_callback *cb)
>       return skb->len ? : ret;
>  }
>  
> +static int rtnl_fill_mcaddr(struct sk_buff *skb, const struct net_device 
> *dev,
> +                         const struct netdev_hw_addr *ha, u32 portid,
> +                         u32 seq, unsigned int flags)
> +{

[ ... ]

> +     if (nla_put(skb, IFA_MULTICAST, dev->addr_len, ha->addr) ||
> +         nla_put_u32(skb, IFA_MC_USERS, ha->refcount)) {

[Severity: Medium]

Can this nla_put() copy uninitialized slab bytes into IFA_MULTICAST when
dev->addr_len grew after the entry was created?

netdev_hw_addr entries are allocated with plain kmalloc() and only
addr_len bytes are initialized, and the length used at creation time is
not recorded anywhere:

net/core/dev_addr_lists.c:__hw_addr_create() {
        alloc_size = sizeof(*ha);
        if (alloc_size < L1_CACHE_BYTES)
                alloc_size = L1_CACHE_BYTES;
        ha = kmalloc(alloc_size, GFP_ATOMIC);
        if (!ha)
                return NULL;
        memcpy(ha->addr, addr, addr_len);
        ...
}

__dev_mc_add() passes dev->addr_len as it is at add time:

net/core/dev_addr_lists.c:__dev_mc_add() {
        err = __hw_addr_add_ex(&dev->mc, addr, dev->addr_len,
                               NETDEV_HW_ADDR_T_MULTICAST, global, false,
                               0, false);
}

while rtnl_fill_mcaddr() emits the current dev->addr_len.  One sequence
that produces a mismatch:

  1. create a tap device (ARPHRD_ETHER, addr_len 6)
  2. SIOCADDMULTI via dev_ifsioc() -> dev_mc_add_global(), which only
     requires netif_device_present(), so the device may be down; 6 bytes
     of ha->addr are initialized
  3. TUNSETLINK with ARPHRD_TUNNEL6 in drivers/net/tun.c:

        tun->dev->type = (int) arg;
        tun->dev->addr_len = tun_get_addr_len(tun->dev->type);

     which raises addr_len to 16 and does not purge dev->mc
  4. run the new RTM_GETMULTICAST/AF_PACKET dump

Would step 4 then emit 16 bytes out of a 6-byte-initialized ha->addr,
handing 10 bytes of never-written slab memory to user space?  The read
stays inside ha->addr[MAX_ADDR_LEN], so this is a disclosure of
uninitialized memory rather than an out-of-bounds access.

The same bytes are already visible through /proc/net/dev_mcast, which
prints "%*phN" with dev->addr_len, so the underlying unzeroed tail is
older than this patch, but this adds a second path to it.  Would it make
sense to either bound the emitted length or zero the address tail in
__hw_addr_create()?

> +             nlmsg_cancel(skb, nlh);
> +             return -EMSGSIZE;
> +     }
> +
> +     nlmsg_end(skb, nlh);
> +     return 0;
> +}
> +
> +static int rtnl_dump_mcaddr_dev(struct net_device *dev, struct sk_buff *skb,
> +                             struct netlink_callback *cb, int *s_addr_idx,
> +                             unsigned int flags)
> +{
> +     struct netdev_hw_addr *ha;
> +     int addr_idx = 0;
> +     int err = 0;
> +
> +     netif_addr_lock_bh(dev);
> +     netdev_for_each_mc_addr(ha, dev) {
> +             if (addr_idx < *s_addr_idx) {
> +                     addr_idx++;
> +                     continue;
> +             }
> +             err = rtnl_fill_mcaddr(skb, dev, ha, NETLINK_CB(cb->skb).portid,
> +                                    cb->nlh->nlmsg_seq, flags);
> +             if (err < 0)
> +                     break;
> +             addr_idx++;
> +     }
> +     netif_addr_unlock_bh(dev);
> +
> +     *s_addr_idx = err < 0 ? addr_idx : 0;

[Severity: Medium]

Should this dump also maintain a generation counter and call
nl_dump_check_consistent() so interrupted dumps get NLM_F_DUMP_INTR?

Resume state here is purely positional (ctx->ifindex plus ctx->addr_idx)
and neither RTNL nor netif_addr_lock is held between two invocations of
the callback.  netlink_dump() turns the -EMSGSIZE into a continued dump:

net/netlink/af_netlink.c:netlink_dump() {
        if (nlk->dump_done_errno == -EMSGSIZE && skb->len)
                nlk->dump_done_errno = skb->len;
}

so entries added to or removed from dev->mc in that window shift the
index and entries can be silently repeated or skipped.

cb->seq is never assigned in rtnl_dump_mcaddr(), so the check in
nl_dump_check_consistent():

        if (cb->prev_seq && cb->seq != cb->prev_seq)
                nlh->nlmsg_flags |= NLM_F_DUMP_INTR;

can never fire and user space has no way to notice the loss.  The AF_INET
sibling of the same message type does both halves: inet_dump_addr() sets

        cb->seq = inet_base_seq(tgt_net);

and in_dev_dump_ifmcaddr() calls nl_dump_check_consistent(cb,
nlmsg_hdr(skb)) for every emitted message.

> +
> +     return err;
> +}

[ ... ]

> +static int rtnl_dump_mcaddr(struct sk_buff *skb, struct netlink_callback *cb)
> +{

[ ... ]

> +     for_each_netdev_dump(net, dev, ctx->ifindex) {
> +             err = rtnl_dump_mcaddr_dev(dev, skb, cb, &ctx->addr_idx,
> +                                        flags);
> +             if (err < 0)
> +                     break;
> +     }

[Severity: Medium]

Related to the above, can a stale ctx->addr_idx be applied to the wrong
device here?

On -EMSGSIZE, rtnl_dump_mcaddr_dev() keeps the offset in ctx->addr_idx and
the break skips the ifindex++ of for_each_netdev_dump(), so ctx->ifindex
still names the interrupted device.  If that device is unregistered before
the dump resumes:

include/linux/netdevice.h:
        #define for_each_netdev_dump(net, d, ifindex)                           
\
                for (; (d = xa_find(&(net)->dev_by_index, &ifindex,             
\
                                    ULONG_MAX, XA_PRESENT)); ifindex++)

xa_find() with XA_PRESENT returns the next device that does exist, and the
non-zero ctx->addr_idx is passed unchanged into rtnl_dump_mcaddr_dev(), so

                if (addr_idx < *s_addr_idx) {
                        addr_idx++;
                        continue;
                }

would skip the first ctx->addr_idx multicast entries of an unrelated
device.  Should ctx->addr_idx be reset when the resumed ifindex does not
match the recorded one?

> +out:
> +     rcu_read_unlock();
> +     return err;
> +}
> +

[ ... ]

> @@ -7251,6 +7381,8 @@ static const struct rtnl_msg_handler 
> rtnetlink_rtnl_msg_handlers[] __initconst =
>       {.msgtype = RTM_SETSTATS, .doit = rtnl_stats_set},
>       {.msgtype = RTM_NEWLINKPROP, .doit = rtnl_newlinkprop},
>       {.msgtype = RTM_DELLINKPROP, .doit = rtnl_dellinkprop},
> +     {.protocol = PF_PACKET, .msgtype = RTM_GETMULTICAST,
> +      .dumpit = rtnl_dump_mcaddr, .flags = RTNL_FLAG_DUMP_UNLOCKED},

[Severity: Low]

This isn't a bug, but as of this commit alone the getmulticast op in
Documentation/netlink/specs/rt-addr.yaml still reads "Get / dump
IPv4/IPv6 multicast addresses." while the same op now also serves
AF_PACKET requests with link-layer address semantics.

The follow-up patch in the series, "netlink: specs: rt-addr: document
AF_PACKET multicast dumps", rewrites that doc to cover ifa-family
selecting AF_INET, AF_INET6 or AF_PACKET and the permanent-flag meaning,
so the spec and the implementation do agree at the end of the series.
Only noting it in case the two patches are ever applied separately.

>       {.protocol = PF_BRIDGE, .msgtype = RTM_GETLINK,
>        .dumpit = rtnl_bridge_getlink},
>       {.protocol = PF_BRIDGE, .msgtype = RTM_DELLINK,

-- 
Sashiko AI review · 
https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260905093932.43726-1-sigefriedhyy%40gmail.com

Reply via email to