[PATCH 1/2] macsec_linux: Preserve shared Tx SC ownership across reauthentication

Eugen Hristev ehristev at kernel.org
Wed Oct 21 23:28:24 PDT 2026


EAP reauthentication can create a replacement MKA participant while the
previous participant continues to carry traffic. Both participants have
the same local SCI and use the same Linux MACsec interface, but the Linux
driver keeps only one link handle and one created_link flag per instance.

The second create_transmit_sc() attempts to create the interface again.
When it finds the existing interface, it clears created_link even if this
driver instance originally created that interface, and replaces the
shared libnl link handle. When either participant is subsequently removed,
delete_transmit_sc() releases that handle without considering the other
participant:

  macsec_linux: link already exists, using it
  macsec_linux: ... create_transmit_sc: ifi=163 ifname=macsec0
  KaY: participant removed
  macsec_linux: macsec0: delete_transmit_sc -> ...
  macsec_linux: we didn't create the link, leave it alone

The surviving participant can still enable an SA through generic netlink,
but try_commit() silently returns success with drv->link == NULL. Thus the
next rekey can enable the new Tx SA without sending the encoding-SA update:

  macsec_linux: macsec0: enable_transmit_sa -> 2 on ...
  CP: CP entering state TRANSMITTING
  CP: CP entering state RETIRE
  macsec_linux: macsec0: delete_transmit_sa -> 1 on ...

There is no intervening "try_commit encoding_sa=2". The kernel still
selects the deleted SA 1, while SA 2 is the only remaining SA. Traffic
stops; explicitly selecting SA 2 restores it. Clearing created_link also
causes macsec0 to be left behind at shutdown despite being created by this
driver instance. The replaced libnl handle is leaked as well.

To fix this, the following is implemented : track users of the shared Tx
SC and its SCI. Reuse the existing handle for matching participants without
changing the original ownership.
Release the handle and delete an owned kernel interface only on the final
release; leave externally created interfaces in place.
Reject conflicting SCs and roll back a new reference if committing pending
settings fails. Allocate the persistent handle before creating the kernel
interface. Also report a missing handle when a pending encoding-SA update
cannot be applied rather than treating that update as a successful no-op.

To validate this scenario before and after this fix:
Add macsec_hostapd_eap_reauth_rekey test to reproduce the delayed failure
over a veth pair using software MACsec. Trigger EAPOL_REAUTH, wait beyond
the MKA participant lifetime, then raise the authenticator's active Tx PN
to 0xC1000000.
Require the encoding AN to change and verify bidirectional unicast and
broadcast connectivity. The current code without the fix timed out with
"MACsec TxSC did not switch from AN 0"; the fixed setup completed the AN
switch and connectivity checks. This does not require hardware offload or
a kernel driver change.

Assisted-by: Github Copilot
Signed-off-by: Eugen Hristev <ehristev at kernel.org>
---
 src/drivers/driver_macsec_linux.c | 123 ++++++++++++++++++++++--------
 tests/hwsim/test_macsec.py        |  50 +++++++++++-
 2 files changed, 139 insertions(+), 34 deletions(-)

diff --git a/src/drivers/driver_macsec_linux.c b/src/drivers/driver_macsec_linux.c
index 57bea3b42244..161bb6111aa3 100644
--- a/src/drivers/driver_macsec_linux.c
+++ b/src/drivers/driver_macsec_linux.c
@@ -66,6 +66,8 @@ struct macsec_drv_data {
 	int use_pae_group_addr;
 
 	bool created_link;
+	unsigned int txsc_users;
+	u64 txsc_sci;
 
 	bool controlled_port_enabled;
 	bool controlled_port_enabled_set;
@@ -186,8 +188,11 @@ static int try_commit(struct macsec_drv_data *drv)
 	if (!drv->sk)
 		return 0;
 
-	if (!drv->link)
+	if (!drv->link) {
+		if (drv->encoding_sa_set)
+			return -ENODEV;
 		return 0;
+	}
 
 	if (drv->controlled_port_enabled_set) {
 		struct rtnl_link *change = rtnl_link_alloc();
@@ -1144,6 +1149,36 @@ static struct rtnl_link * lookup_sc(struct nl_cache *cache, int parent, u64 sci,
 }
 
 
+static int macsec_drv_release_transmit_sc(struct macsec_drv_data *drv)
+{
+	int err = 0;
+
+	if (!drv->txsc_users)
+		return -EINVAL;
+
+	if (--drv->txsc_users)
+		return 0;
+
+	if (drv->created_link) {
+		if (!drv->link || !drv->sk)
+			err = -ENODEV;
+		else
+			err = rtnl_link_delete(drv->sk, drv->link);
+		if (err < 0)
+			wpa_printf(MSG_ERROR, DRV_PREFIX "couldn't delete link: %d",
+				   err);
+	}
+
+	rtnl_link_put(drv->link);
+	drv->link = NULL;
+	drv->created_link = false;
+	drv->txsc_sci = 0;
+	drv->encoding_sa_set = false;
+
+	return err;
+}
+
+
 /**
  * macsec_drv_create_transmit_sc - Create secure connection for transmit
  * @priv: private driver interface data from init()
@@ -1172,19 +1207,38 @@ static int macsec_drv_create_transmit_sc(
 		return -1;
 	}
 
+	sci = mka_sci_u64(&sc->sci);
+	if (drv->txsc_users) {
+		if (drv->txsc_sci != sci || !drv->link) {
+			wpa_printf(MSG_ERROR, DRV_PREFIX
+				   "transmit SC conflicts with existing link state");
+			return -EBUSY;
+		}
+
+		drv->txsc_users++;
+		wpa_printf(MSG_DEBUG, DRV_PREFIX
+			   "sharing transmit SC (users=%u)", drv->txsc_users);
+		err = try_commit(drv);
+		if (err < 0)
+			macsec_drv_release_transmit_sc(drv);
+		return err;
+	}
+
 	link = rtnl_link_macsec_alloc();
 	if (!link) {
 		wpa_printf(MSG_ERROR, DRV_PREFIX "couldn't allocate link");
 		return -1;
 	}
+	drv->link = rtnl_link_macsec_alloc();
+	if (!drv->link) {
+		rtnl_link_put(link);
+		wpa_printf(MSG_ERROR, DRV_PREFIX "couldn't allocate link handle");
+		return -1;
+	}
 
 	rtnl_link_set_link(link, drv->parent_ifi);
 
-	sci = mka_sci_u64(&sc->sci);
 	rtnl_link_macsec_set_sci(link, sci);
-
-	drv->created_link = true;
-
 	if (drv->cipher_suite_set) {
 		cs = drv->cipher_suite;
 		drv->cipher_suite_set = false;
@@ -1195,13 +1249,16 @@ static int macsec_drv_create_transmit_sc(
 	if (err == -NLE_BUSY) {
 		wpa_printf(MSG_INFO,
 			   DRV_PREFIX "link already exists, using it");
-		drv->created_link = false;
 	} else if (err < 0) {
 		rtnl_link_put(link);
+		rtnl_link_put(drv->link);
+		drv->link = NULL;
 		wpa_printf(MSG_ERROR, DRV_PREFIX "couldn't create link: err %d",
 			   err);
 		drv->created_link = false;
 		return err;
+	} else {
+		drv->created_link = true;
 	}
 
 	rtnl_link_put(link);
@@ -1209,6 +1266,12 @@ static int macsec_drv_create_transmit_sc(
 	nl_cache_refill(drv->sk, drv->link_cache);
 	link = lookup_sc(drv->link_cache, drv->parent_ifi, sci, cs);
 	if (!link) {
+		rtnl_link_put(drv->link);
+		drv->link = NULL;
+		if (drv->created_link)
+			wpa_printf(MSG_ERROR, DRV_PREFIX
+				   "created MACsec link but couldn't find it to manage");
+		drv->created_link = false;
 		wpa_printf(MSG_ERROR, DRV_PREFIX "couldn't find link");
 		return -1;
 	}
@@ -1221,18 +1284,17 @@ static int macsec_drv_create_transmit_sc(
 	os_strlcpy(drv->ifname, ifname, sizeof(drv->ifname));
 	rtnl_link_put(link);
 
-	drv->link = rtnl_link_macsec_alloc();
-	if (!drv->link) {
-		wpa_printf(MSG_ERROR, DRV_PREFIX "couldn't allocate link");
-		drv->created_link = false;
-		return -1;
-	}
-
 	rtnl_link_set_name(drv->link, drv->ifname);
+	drv->txsc_sci = sci;
+	drv->txsc_users = 1;
 
 	/* In case some settings have already been done but we couldn't apply
 	 * them. */
-	return try_commit(drv);
+	err = try_commit(drv);
+	if (err < 0)
+		macsec_drv_release_transmit_sc(drv);
+
+	return err;
 }
 
 
@@ -1245,34 +1307,29 @@ static int macsec_drv_create_transmit_sc(
 static int macsec_drv_delete_transmit_sc(void *priv, struct transmit_sc *sc)
 {
 	struct macsec_drv_data *drv = priv;
-	int err;
+	u64 sci = mka_sci_u64(&sc->sci);
 
 	wpa_printf(MSG_DEBUG, DRV_PREFIX "%s: delete_transmit_sc -> " SCISTR,
 		   drv->ifname, SCI2STR(sc->sci.addr, sc->sci.port));
 
-	if (!drv->sk)
-		return 0;
-
-	if (!drv->created_link) {
-		rtnl_link_put(drv->link);
-		drv->link = NULL;
-		wpa_printf(MSG_DEBUG, DRV_PREFIX
-			   "we didn't create the link, leave it alone");
-		return 0;
+	if (!drv->txsc_users || drv->txsc_sci != sci) {
+		wpa_printf(MSG_ERROR, DRV_PREFIX
+			   "transmit SC release does not match active link");
+		return -EINVAL;
 	}
 
-	if (!drv->link) {
-		wpa_printf(MSG_ERROR, DRV_PREFIX "no link to delete");
-		return -1;
+	if (drv->txsc_users > 1) {
+		wpa_printf(MSG_DEBUG, DRV_PREFIX
+			   "keeping shared transmit SC (users=%u)",
+			   drv->txsc_users - 1);
+		return macsec_drv_release_transmit_sc(drv);
 	}
 
-	err = rtnl_link_delete(drv->sk, drv->link);
-	if (err < 0)
-		wpa_printf(MSG_ERROR, DRV_PREFIX "couldn't delete link");
-	rtnl_link_put(drv->link);
-	drv->link = NULL;
+	if (!drv->created_link)
+		wpa_printf(MSG_DEBUG, DRV_PREFIX
+			   "we didn't create the link, leave it alone");
 
-	return err;
+	return macsec_drv_release_transmit_sc(drv);
 }
 
 
diff --git a/tests/hwsim/test_macsec.py b/tests/hwsim/test_macsec.py
index e76d0cc31b54..8402f057b732 100644
--- a/tests/hwsim/test_macsec.py
+++ b/tests/hwsim/test_macsec.py
@@ -190,6 +190,22 @@ def log_ip_macsec():
     out, err = cmd.communicate()
     logger.info("ip macsec:\n" + out.decode())
 
+def macsec_tx_an(ifname):
+    out = subprocess.check_output(["ip", "macsec", "show", ifname])
+    for line in out.decode().splitlines():
+        if line.strip().startswith("TXSC:"):
+            return int(line.rsplit(" on SA ", 1)[1])
+    raise Exception("MACsec TxSC not found for " + ifname)
+
+def wait_macsec_tx_an(ifname, old_an, timeout=20):
+    end = time.time() + timeout
+    while time.time() < end:
+        an = macsec_tx_an(ifname)
+        if an != old_an:
+            return an
+        time.sleep(0.25)
+    raise Exception("MACsec TxSC did not switch from AN %d" % old_an)
+
 def log_ip_link():
     cmd = subprocess.Popen(["ip", "link", "show"],
                            stdout=subprocess.PIPE)
@@ -822,6 +838,15 @@ def test_macsec_hostapd_eap(dev, apdev, params):
     finally:
         cleanup_macsec_hostapd()
 
+def test_macsec_hostapd_eap_reauth_rekey(dev, apdev, params):
+    """MACsec EAP reauthentication followed by a Tx-SA rekey"""
+    try:
+        run_macsec_hostapd_eap(dev, apdev, params,
+                               "macsec_hostapd_eap_reauth_rekey",
+                               reauth_rekey=True)
+    finally:
+        cleanup_macsec_hostapd()
+
 def test_macsec_hostapd_eap_psk(dev, apdev, params):
     """MACsec EAP-PSK with hostapd"""
     try:
@@ -832,7 +857,7 @@ def test_macsec_hostapd_eap_psk(dev, apdev, params):
 
 def run_macsec_hostapd_eap(dev, apdev, params, prefix, integ_only=False,
                            port0=None, port1=None, expect_failure=False,
-                           eap_psk=False):
+                           eap_psk=False, reauth_rekey=False):
     add_veth()
 
     cap_veth0 = os.path.join(params['logdir'], prefix + ".veth0.pcap")
@@ -890,6 +915,29 @@ def run_macsec_hostapd_eap(dev, apdev, params, prefix, integ_only=False,
     macsec_ifname0 = wpas0.get_driver_status_field("parent_ifname")
     macsec_ifname1 = hapd.get_driver_status_field("parent_ifname")
 
+    if reauth_rekey:
+        wpas0.dump_monitor()
+        sta_addr = wpas0.get_status_field("address")
+        res = hapd.request("EAPOL_REAUTH " + sta_addr)
+        if "OK" not in res:
+            raise Exception("Hostapd failed to start EAP reauthentication: " +
+                            res)
+        if wpas0.wait_event(["CTRL-EVENT-EAP-STARTED"], timeout=10) is None:
+            raise Exception("EAP reauthentication did not start")
+        if wpas0.wait_event(["CTRL-EVENT-EAP-SUCCESS"], timeout=15) is None:
+            raise Exception("EAP reauthentication did not succeed")
+        wait_mka_done(wpa, hostapd=True)
+
+        # MKA removes the old participant after its six-second lifetime.
+        time.sleep(7)
+        old_an = macsec_tx_an(macsec_ifname1)
+        subprocess.check_call(["ip", "macsec", "set", macsec_ifname1,
+                               "tx", "sa", str(old_an), "pn", "0xC1000000"])
+        new_an = wait_macsec_tx_an(macsec_ifname1, old_an)
+        logger.info("MACsec TxSC changed from AN %d to AN %d after rekey",
+                    old_an, new_an)
+        wait_mka_done(wpa, hostapd=True)
+
     cmd[2] = WlantestCapture(macsec_ifname0, cap_macsec0)
     cmd[3] = WlantestCapture(macsec_ifname1, cap_macsec1)
     time.sleep(0.5)
-- 
2.43.0




More information about the Hostap mailing list