Netdev List
 help / color / mirror / Atom feed
From: Thomas Martitz <t.martitz@fritz.com>
To: Simon Horman <horms@kernel.org>
Cc: andrew+netdev@lunn.ch, davem@davemloft.net, edumazet@google.com,
	kuba@kernel.org, pabeni@redhat.com, netdev@vger.kernel.org,
	linux-kernel@vger.kernel.org
Subject: Re: [PATCH v5 1/1] macvlan: allow source mode devices along with passthru
Date: Wed, 7 Oct 2026 18:55:14 +0200	[thread overview]
Message-ID: <f9780706-f0e5-41e6-b206-af0a5873aa88@fritz.com> (raw)
In-Reply-To: <20260921092359.91614-1-horms@kernel.org>

Hello,

I took a few free slots during LPC 2026 to address
the concerns raised by AI review, almost all of which were
valid.

I also expanded my local test suite to catch all of these.

I'll post v6 in shortly.

m 21.09.26 um 11:23 schrieb Simon Horman:
> This is an AI-generated review of your patch. The human sending this
> email has considered the AI review valid, or at least plausible.
> Full review at: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260917101044.2654867-1-t.martitz@fritz.com?part=1
> ---
> macvlan: allow source mode devices along with passthru
> 
> This allows one passthru macvlan to coexist with source mode macvlans on
> the same lower device, by converting several per-device decisions from the
> port wide MACVLAN_F_PASSTHRU flag to vlan->mode and by moving the lower
> device MAC restore from macvlan_port_destroy() into macvlan_uninit().
> 
> A few questions about the conversion below.
> 
>> diff --git a/drivers/net/macvlan.c b/drivers/net/macvlan.c
>> index afad90b9222a2..76c11e5260334 100644
>> --- a/drivers/net/macvlan.c
>> +++ b/drivers/net/macvlan.c
>> @@ -83,6 +83,11 @@ static inline void macvlan_set_passthru(struct macvlan_port *port)
>>  	port->flags |= MACVLAN_F_PASSTHRU;
>>  }
>>  
>> +static inline void macvlan_clear_passthru(struct macvlan_port *port)
>> +{
>> +	port->flags &= ~MACVLAN_F_PASSTHRU;
>> +}
>> +
>>  static inline bool macvlan_addr_change(const struct macvlan_port *port)
>>  {
>>  	return port->flags & MACVLAN_F_ADDRCHANGE;
>> @@ -217,8 +222,7 @@ static bool macvlan_addr_busy(const struct macvlan_port *port,
>>  	 * currently in use by the underlying device or
>>  	 * another macvlan.
>>  	 */
>> -	if (!macvlan_passthru(port) && !macvlan_addr_change(port) &&
>> -	    ether_addr_equal_64bits(port->dev->dev_addr, addr))
>> +	if (ether_addr_equal_64bits(port->dev->dev_addr, addr))
>>  		return true;
>>  
>>  	if (macvlan_hash_lookup(port, addr))
>> @@ -637,7 +641,7 @@ static int macvlan_open(struct net_device *dev)
>>  	struct net_device *lowerdev = vlan->lowerdev;
>>  	int err;
>>  
>> -	if (macvlan_passthru(vlan->port)) {
>> +	if (vlan->mode == MACVLAN_MODE_PASSTHRU) {
>>  		if (!(vlan->flags & MACVLAN_FLAG_NOPROMISC)) {
>>  			err = dev_set_promiscuity(lowerdev, 1);
>>  			if (err < 0)
> 
> [Severity: High]
> 
> macvlan_open() and macvlan_stop() are converted to the per-device
> vlan->mode test here, but macvlan_change_rx_flags() still keys off the
> port flag:
> 
> drivers/net/macvlan.c:macvlan_change_rx_flags() {
> 	if (dev->flags & IFF_UP) {
> 		if (change & IFF_ALLMULTI)
> 			dev_set_allmulti(lowerdev, dev->flags & IFF_ALLMULTI ? 1 : -1);
> 		if (!macvlan_passthru(vlan->port) && change & IFF_PROMISC)
> 			dev_set_promiscuity(lowerdev,
> 					    dev->flags & IFF_PROMISC ? 1 : -1);
> 	}
> }
> 
> Once a passthru device exists the port flag is set for every sibling, so
> live IFF_PROMISC transitions of a source mode macvlan are dropped, while
> macvlan_open() and macvlan_stop() still take and release the lower device
> promiscuity reference based on dev->flags.
> 
> Can this unbalance lowerdev->promiscuity?
> 
>   ip link add pt link eth0 type macvlan mode passthru
>   ip link add src0 link eth0 type macvlan mode source
>   ip link set src0 promisc on
>   ip link set src0 up	  macvlan_open() -> dev_set_promiscuity(lowerdev, 1)
>   ip link set src0 promisc off	  macvlan_change_rx_flags() skips the -1
>   ip link set src0 down	  macvlan_stop() sees IFF_PROMISC clear, no -1
> 
> That leaks a +1 on the physical device. The reverse order (up without
> promisc, promisc on while up, then down) issues an unmatched
> dev_set_promiscuity(lowerdev, -1), and __dev_set_promiscuity() computes
> 0 + (-1) as 0xFFFFFFFF, takes the "flags = old_flags | IFF_PROMISC"
> branch and returns 0, so the device stays pinned promiscuous with no
> error reported. If the passthru device held the only reference, the count
> instead reaches 0, IFF_PROMISC is cleared and the hardware filter is
> reprogrammed while the passthru macvlan still needs it.
> 
> Before this patch a passthru port could hold no other device, so the port
> wide test in macvlan_change_rx_flags() was equivalent to the per-device
> one. Should that call site be converted too?

Makes sense, I'll follow up with that change.

> 
>> @@ -712,7 +716,7 @@ static int macvlan_stop(struct net_device *dev)
>>  	dev_uc_unsync(lowerdev, dev);
>>  	dev_mc_unsync(lowerdev, dev);
>>  
>> -	if (macvlan_passthru(vlan->port)) {
>> +	if (vlan->mode == MACVLAN_MODE_PASSTHRU) {
>>  		if (!(vlan->flags & MACVLAN_FLAG_NOPROMISC))
>>  			dev_set_promiscuity(lowerdev, -1);
>>  		goto hash_del;
>> @@ -737,17 +741,18 @@ static int macvlan_sync_address(struct net_device *dev,
>>  	struct macvlan_dev *vlan = netdev_priv(dev);
>>  	struct net_device *lowerdev = vlan->lowerdev;
>>  	struct macvlan_port *port = vlan->port;
>> +	bool passthru_dev = vlan->mode == MACVLAN_MODE_PASSTHRU;
>>  	int err;
>>  
>>  	if (!(dev->flags & IFF_UP)) {
>>  		/* Just copy in the new address */
>>  		eth_hw_addr_set(dev, addr);
>>  	} else {
>> -		/* Rehash and update the device filters */
>> -		if (macvlan_addr_busy(vlan->port, addr))
>> -			return -EADDRINUSE;
>> +		if (!passthru_dev) {
>> +			/* Rehash and update the device filters */
>> +			if (macvlan_addr_busy(vlan->port, addr))
>> +					return -EADDRINUSE;
>>  
>> -		if (!macvlan_passthru(port)) {
>>  			err = dev_uc_add(lowerdev, addr);
>>  			if (err)
>>  				return err;
> 
> [Severity: Medium]
> 
> The macvlan_addr_busy() call used to run for the passthru device as well,
> and its macvlan_hash_lookup() half caught collisions with siblings on the
> same port. Moving it inside "if (!passthru_dev)" drops that check for the
> passthru device.
> 
> Can a passthru device now take the address of a live source mode sibling?
> macvlan_set_mac_address() returns before its own busy check:
> 
> drivers/net/macvlan.c:macvlan_set_mac_address() {
> 	if (READ_ONCE(vlan->mode) == MACVLAN_MODE_PASSTHRU) {
> 		macvlan_set_addr_change(vlan->port);
> 		return dev_set_mac_address(vlan->lowerdev, addr, NULL);
> 	}
> 
> 	if (macvlan_addr_busy(vlan->port, addr->__data))
> 		return -EADDRINUSE;
> 
> so with pt (passthru) and src0 (source) both up, "ip link set pt address
> <src0 mac>" reprograms the lower device and comes back through
> macvlan_device_event() -> macvlan_sync_address() with passthru_dev true,
> where nothing validates it anymore.
> 
> Both macvlans then end up hashed under the same address, and src0 can no
> longer be brought up because macvlan_addr_busy() now compares against
> port->dev->dev_addr unconditionally:

I'll move the macvlan_addr_busy() check in macvlan_set_mac_address() up 
so that passthru devices are covered as well. That should catch if 
a passthru interface (and consequently the lower device) is assigned the same
mac as one of the existing source interfaces.


> 
> drivers/net/macvlan.c:macvlan_open() {
> 	err = -EADDRINUSE;
> 	if (macvlan_addr_busy(vlan->port, dev->dev_addr))
> 		goto out;
> 
>> @@ -757,7 +762,7 @@ static int macvlan_sync_address(struct net_device *dev,
>>  
>>  		macvlan_hash_change_addr(vlan, addr);
>>  	}
>> -	if (macvlan_passthru(port) && !macvlan_addr_change(port)) {
>> +	if (passthru_dev && !macvlan_addr_change(port)) {
>>  		/* Since addr_change isn't set, we are here due to lower
>>  		 * device change.  Save the lower-dev address so we can
>>  		 * restore it later.
>> @@ -979,7 +984,26 @@ static void macvlan_uninit(struct net_device *dev)
>>  
>>  	macvlan_flush_sources(port, vlan);
>>  	port->count -= 1;
>> -	if (!port->count)
>> +
>> +	if (vlan->mode == MACVLAN_MODE_PASSTHRU) {
>> +		/* If the lower device address has been changed by passthru
>> +		 * macvlan, put it back.
>> +		 *
>> +		 * Caution: This triggers NETDEV_CHANGEADDR on
>> +		 * the lower device. But we're in the green because
>> +		 * the passthru interface is already off the list.
>> +		 */
>> +		if(!ether_addr_equal(port->dev->dev_addr, port->perm_addr)) {
>> +			struct sockaddr_storage ss;
>> +
>> +			ss.ss_family = port->dev->type;
>> +			memcpy(&ss.__data, port->perm_addr, port->dev->addr_len);
>> +			dev_set_mac_address(port->dev, &ss, NULL);
>> +		}
>> +		macvlan_clear_passthru(port);
>> +	}
>> +
>> +	if (port->count == 0)
>>  		macvlan_port_destroy(port->dev);
>>  }
> 
> [Severity: High]
> 
> The restore now runs at passthru device lifetime, but the matching save
> into port->perm_addr is still done once at port creation:
> 
> drivers/net/macvlan.c:macvlan_port_create() {
> 	ether_addr_copy(port->perm_addr, dev->dev_addr);
> 
> and it is only refreshed while a passthru device already exists:
> 
> drivers/net/macvlan.c:macvlan_device_event() {
> 	case NETDEV_CHANGEADDR:
> 		if (!macvlan_passthru(port))
> 			return NOTIFY_DONE;
> 
> Since a port can now be created by, and outlive, source mode only devices,
> can this write a stale address onto the lower device?
> 
>   ip link add src0 link eth0 type macvlan mode source
> 	  macvlan_port_create() saves eth0 mac A into port->perm_addr
>   ip link set eth0 address B
> 	  macvlan_device_event() returns NOTIFY_DONE, perm_addr stays A
>   ip link add pt link eth0 type macvlan mode passthru
> 	  accepted now, eth_hw_addr_inherit() gives pt mac B
>   ip link del pt
> 	  macvlan_uninit() sees B != A and sets eth0 back to A
> 
> eth0 ends up with an address the macvlan never installed. Should
> port->perm_addr be re-captured next to the eth_hw_addr_inherit() call in
> macvlan_common_newlink() when the passthru device is created?

The suggestion is spot-on, I think. I came to the same conclusion.

Actually the remembering the lowerdev mac in port->perm_addr
can be moved as it's only needed once there is a passthru interface,
and never for regular vepa/source/bridge combinations.

> 
> Two related questions on the same block:
> 
> macvlan_uninit() is also reached from the register_netdevice() and
> netdev_upper_dev_link() failure paths in macvlan_common_newlink(), for a
> passthru device that never touched the lower MAC. Does the restore fire
> there too?

With the above change the the MAC addresses are in sync within
macvlan_common_newlink().

> 
> macvlan_clear_passthru() clears MACVLAN_F_PASSTHRU but not
> MACVLAN_F_ADDRCHANGE. In a port that now survives the passthru device, can
> a stale addr change bit be observed by the next passthru device created on
> that port?

The MACVLAN_F_ADDRCHANGE is cleared eagerly by macvlan_sync_address().
It is only set during the chain
set_mac_address(passthrough, addr)
-> dev_set_mac_address(lowerdev, addr)
-> macvlan_device_event()
-> macvlan_sync_address()

It is always clear in macvlan_unit().


> 
> [Severity: Low]
> 
> These are not bugs, but checkpatch will complain about a few things
> introduced here:
> 
>   if(!ether_addr_equal(port->dev->dev_addr, port->perm_addr)) {
> 
> missing space after if.
> 
> In macvlan_sync_address() the return is indented by two extra tabs:
> 
> 		if (macvlan_addr_busy(vlan->port, addr))
> 				return -EADDRINUSE;
> 
> In macvlan_common_newlink() the if branch has braces and the else branch
> does not. There is also a typo, "passhtrough", in the new
> macvlan_changelink() comment, and some of the new comment lines run past
> 80 columns.
> 

Will be fixed.

> [ ... ]
> 
>> @@ -1310,18 +1334,6 @@ static void macvlan_port_destroy(struct net_device *dev)
>>  		kfree_skb(skb);
>>  	}
>>  
>> -	/* If the lower device address has been changed by passthru
>> -	 * macvlan, put it back.
>> -	 */
>> -	if (macvlan_passthru(port) &&
>> -	    !ether_addr_equal(port->dev->dev_addr, port->perm_addr)) {
>> -		struct sockaddr_storage ss;
>> -
>> -		ss.ss_family = port->dev->type;
>> -		memcpy(&ss.__data, port->perm_addr, port->dev->addr_len);
>> -		dev_set_mac_address(port->dev, &ss, NULL);
>> -	}
>> -
>>  	kfree(port);
>>  }
>>  
>> @@ -1523,15 +1535,6 @@ int macvlan_common_newlink(struct net_device *dev,
>>  	}
>>  	port = macvlan_port_get_rtnl(lowerdev);
>>  
>> -	/* Only 1 macvlan device can be created in passthru mode */
>> -	if (macvlan_passthru(port)) {
>> -		/* The macvlan port must be not created this time,
>> -		 * still goto destroy_macvlan_port for readability.
>> -		 */
>> -		err = -EINVAL;
>> -		goto destroy_macvlan_port;
>> -	}
>> -
>>  	vlan->lowerdev = lowerdev;
>>  	vlan->dev      = dev;
>>  	vlan->port     = port;
>> @@ -1544,12 +1547,31 @@ int macvlan_common_newlink(struct net_device *dev,
>>  	if (data && data[IFLA_MACVLAN_FLAGS])
>>  		vlan->flags = nla_get_u16(data[IFLA_MACVLAN_FLAGS]);
>>  
>> +	/* Only 1 macvlan device can be created in passthru mode. There may be
>> +	 * additional source mode devices but nothing else at the moment.
>> +	 *
>> +	 * First check if adding a source mode device to an existing passthru vlan.
>> +	 */
>> +	if (macvlan_passthru(port) && vlan->mode != MACVLAN_MODE_SOURCE) {
>> +		/* The macvlan port must be not created this time,
>> +		 * still goto destroy_macvlan_port for readability.
>> +		 */
>> +		err = -EINVAL;
>> +		goto destroy_macvlan_port;
>> +	}
>> +
>> +	/* Now check if adding a passthru device to an existing set of source mode
>> +	 * devices.
>> +	 */
>>  	if (vlan->mode == MACVLAN_MODE_PASSTHRU) {
>> -		if (port->count) {
>> -			err = -EINVAL;
>> -			goto destroy_macvlan_port;
>> +		struct macvlan_dev *p;
>> +
>> +		list_for_each_entry(p, &port->vlans, list) {
>> +			if (p->mode != MACVLAN_MODE_SOURCE) {
>> +				err = -EINVAL;
>> +				goto destroy_macvlan_port;
>> +			}
>>  		}
>> -		macvlan_set_passthru(port);
>>  		eth_hw_addr_inherit(dev, lowerdev);
>>  	}
>>  
>> @@ -1581,7 +1603,12 @@ int macvlan_common_newlink(struct net_device *dev,
>>  	if (err)
>>  		goto unregister_netdev;
>>  
>> -	list_add_tail_rcu(&vlan->list, &port->vlans);
>> +	/* macvlan_handle_frame expects the (one and only) passthru device first. */
>> +	if (vlan->mode == MACVLAN_MODE_PASSTHRU) {
>> +		macvlan_set_passthru(port);
>> +		list_add_rcu(&vlan->list, &port->vlans);
>> +	} else
>> +		list_add_tail_rcu(&vlan->list, &port->vlans);
> 
> [Severity: Low]
> 
> This isn't a bug as far as I can tell, but the ordering here is the
> reverse of initialise-then-publish: macvlan_set_passthru() makes
> macvlan_handle_frame() take the passthru branch before the device is on
> port->vlans. On teardown the mirror window exists too, since
> macvlan_dellink() does list_del_rcu() while the flag is only cleared later
> in macvlan_uninit().
> 
> I could not find a functional consequence, because the reader bails out in
> both windows:
> 
> drivers/net/macvlan.c:macvlan_handle_frame() {
> 	if (macvlan_passthru(port))
> 		vlan = list_first_or_null_rcu(&port->vlans,
> 					      struct macvlan_dev, list);
> 	else
> 		vlan = macvlan_hash_lookup(port, eth->h_dest);
> 	if (!vlan || READ_ONCE(vlan->mode) == MACVLAN_MODE_SOURCE)
> 		return RX_HANDLER_PASS;
> 
> and rtnl is held across register_netdevice() and this list_add_rcu(), so
> the device cannot be up in the creation window. Would publishing the list
> entry before setting the flag, and using READ_ONCE()/WRITE_ONCE() on
> port->flags which macvlan_handle_frame() reads from softirq context, be
> preferable anyway?
> 
>>  	update_port_bc_queue_len(vlan->port);
>>  	netif_stacked_transfer_operstate(lowerdev, dev);
>>  	linkwatch_fire_event(dev);
>> @@ -1652,19 +1679,23 @@ static int macvlan_changelink(struct net_device *dev,
>>  	if (data && data[IFLA_MACVLAN_MODE]) {
>>  		set_mode = true;
>>  		mode = nla_get_u32(data[IFLA_MACVLAN_MODE]);
>> -		/* Passthrough mode can't be set or cleared dynamically */
>> -		if ((mode == MACVLAN_MODE_PASSTHRU) !=
>> -		    (vlan->mode == MACVLAN_MODE_PASSTHRU))
>> -			return -EINVAL;
>> -		if (vlan->mode == MACVLAN_MODE_SOURCE &&
>> -		    vlan->mode != mode)
>> -			macvlan_flush_sources(vlan->port, vlan);
>> +		if (mode != vlan->mode) {
>> +			/* Passthrough mode can't be set or cleared dynamically,
>> +			 * regardless of existing source interfaces. Furthermore, source
>> +			 * interfaces can't switch modes within a passhtrough port.
>> +			 */
>> +			if (vlan->mode == MACVLAN_MODE_PASSTHRU ||
>> +			    macvlan_passthru(vlan->port))
>> +				return -EINVAL;
>> +			if (vlan->mode == MACVLAN_MODE_SOURCE)
>> +				macvlan_flush_sources(vlan->port, vlan);
>> +		}
>>  	}

I think the change makes sense anyway, to ensure
the passthru interface is first in the list once the passthru
flag is set port-wide.


> 
> [Severity: High]
> 
> The old condition rejected both directions of a passthru transition. The
> new one only rejects leaving passthru (vlan->mode == MACVLAN_MODE_PASSTHRU)
> and any mode change inside an already-passthru port
> (macvlan_passthru(vlan->port)). On a port that has no passthru device, is
> switching a device into passthru still rejected?
> 
>   ip link add m0 link eth0 type macvlan mode vepa
>   ip link set m0 up
>   ip link set m0 type macvlan mode passthru
> 
> macvlan_validate() accepts MACVLAN_MODE_PASSTHRU, neither arm of the new
> test matches, and the tail of macvlan_changelink() commits it:
> 
> 	if (set_mode)
> 		WRITE_ONCE(vlan->mode, mode);
> 
> MACVLAN_F_PASSTHRU is never set and the device is never moved to the list
> head, so the comment right above the check ("Passthrough mode can't be set
> or cleared dynamically") and the invariant documented at the list_add_rcu()
> above no longer hold. Since this patch keys the per-device paths off
> vlan->mode, that device now behaves as passthru where it matters:
> 
>   ip link set m0 address <x>	macvlan_set_mac_address() takes the
> 				passthru branch and calls
> 				dev_set_mac_address(vlan->lowerdev, ...)
> 
> IFLA_MACVLAN_MODE has no rtnl_dev_link_net_capable() check on the lower
> device netns, unlike IFLA_MACVLAN_BC_QUEUE_LEN and IFLA_MACVLAN_BC_CUTOFF,
> and netif_set_mac_address() performs no namespace capability check, so can
> CAP_NET_ADMIN in the netns owning a delegated macvlan rewrite the lower
> device MAC this way?
> 
>   ip link set m0 down		macvlan_stop() takes the passthru branch,
> 				skipping dev_uc_del() and the allmulti
> 				release, and issues an unmatched
> 				dev_set_promiscuity(lowerdev, -1)
> 
> macvlan_uninit() would then also rewrite the lower MAC from
> port->perm_addr, and macvlan_fdb_add()/macvlan_fdb_del() start accepting
> unicast filters. Meanwhile macvlan_handle_frame() still uses the hash path
> because the port flag stayed clear, so the device reports passthru but does
> not receive as passthru.
> 
> Should the "mode == MACVLAN_MODE_PASSTHRU" half of the old test be kept?
> 

Yes, indeed.


>>  
>>  	if (data && data[IFLA_MACVLAN_FLAGS]) {
>>  		__u16 flags = nla_get_u16(data[IFLA_MACVLAN_FLAGS]);
>>  		bool promisc = (flags ^ vlan->flags) & MACVLAN_FLAG_NOPROMISC;
>> -		if (macvlan_passthru(vlan->port) && promisc) {
>> +		if (vlan->mode == MACVLAN_MODE_PASSTHRU && promisc) {
>>  			int err;
>>  
>>  			if (flags & MACVLAN_FLAG_NOPROMISC)
> 
> [ ... ]


-- 
Thomas Martitz <t.martitz@fritz.com>
FRITZ! Technology GmbH, Berlin (Germany)

  reply	other threads:[~2026-10-07 16:55 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-17 10:02 [PATCH v5 0/1] macvlan: allow source mode devices along with passthru Thomas Martitz
2026-09-17 10:10 ` [PATCH v5 1/1] " Thomas Martitz
2026-09-21  9:23   ` Simon Horman
2026-10-07 16:55     ` Thomas Martitz [this message]
2026-09-17 12:29 ` [PATCH v5 0/1] " Thomas Martitz

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=f9780706-f0e5-41e6-b206-af0a5873aa88@fritz.com \
    --to=t.martitz@fritz.com \
    --cc=andrew+netdev@lunn.ch \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=horms@kernel.org \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox