[PATCH 05/15] DSCP: Fix policy invalidation and empty unsolicited requests

Andrei-Alexandru Bleortu me at andrei-z.com
Sun Oct 4 03:41:58 PDT 2026


When a STA rejected a DSCP policy, invalidate_policy_by_id() freed the
policy's members, leaked the policy itself and left a NULL entry in
sta->policies[] without changing num_dscp_policies. The next DSCP Policy
Response, a wildcard DSCP Policy Query, or a reassociation then
dereferenced that NULL entry, crashing hostapd and with it every BSS it
runs. Free the policy and remove it from the array instead.

hostapd_send_unsolicited_dscp_policy_request() sent the frame when it
was longer than 5 octets, but the DSCP Policy Request header alone is
longer than that, so a request with neither Reset nor any QoS Management
element went out. Wi-Fi QoS Management 3.4.3 requires at least one
element when Reset is not set; compare against the header length.

Assisted-by: LLM
Signed-off-by: Andrei-Alexandru Bleortu <me at andrei-z.com>
---
 src/ap/robust_av.c          | 17 +++++++++++------
 tests/hwsim/test_ap_dscp.py | 33 +++++++++++++++++++++++++++++++++
 2 files changed, 44 insertions(+), 6 deletions(-)

diff --git a/src/ap/robust_av.c b/src/ap/robust_av.c
index 775b8b8b7..3aa4ab099 100644
--- a/src/ap/robust_av.c
+++ b/src/ap/robust_av.c
@@ -590,7 +590,7 @@ int hostapd_send_unsolicited_dscp_policy_request(struct hostapd_data *hapd,
 	struct wpabuf *frame = NULL;
 	u8 dialog_token;
 	size_t i;
-	size_t frame_len;
+	size_t frame_len, hdr_len;
 	bool more = false;
 
 	/* TODO: When sending a request, if this is not a resync (reset == 0),
@@ -609,7 +609,7 @@ int hostapd_send_unsolicited_dscp_policy_request(struct hostapd_data *hapd,
 	if (!frame)
 		return -1;
 
-	frame_len = wpabuf_len(frame);
+	hdr_len = frame_len = wpabuf_len(frame);
 
 	for (i = 0; i < num_policies; i++) {
 		struct hostapd_dscp_policy *policy;
@@ -645,9 +645,9 @@ int hostapd_send_unsolicited_dscp_policy_request(struct hostapd_data *hapd,
 		buf[7] |= DSCP_POLICY_CTRL_MORE;
 	}
 
-	/* Only send if the frame contains at least one QoS element beyond the
-	 * header */
-	if (frame && wpabuf_len(frame) > 5 &&
+	/* Without Reset, a request carries at least one QoS Management element
+	 * (Wi-Fi QoS Management 3.4.3) */
+	if (frame && (reset || wpabuf_len(frame) > hdr_len) &&
 	    hostapd_drv_send_action(hapd, hapd->iface->freq, 0, sta->addr,
 				    wpabuf_head(frame), wpabuf_len(frame))) {
 		wpa_printf(MSG_DEBUG, "DSCP: Failed to send policy request to "
@@ -1458,7 +1458,12 @@ static void invalidate_policy_by_id(struct sta_info *sta, u8 policy_id)
 			wpa_printf(MSG_DEBUG, "DSCP: Invalidating policy %u",
 				   policy_id);
 			free_dscp_policy(policy);
-			sta->policies[i] = NULL;
+			os_free(policy);
+			/* Keep the array dense: its users do not expect holes */
+			os_memmove(&sta->policies[i], &sta->policies[i + 1],
+				   (sta->num_dscp_policies - i - 1) *
+				   sizeof(sta->policies[0]));
+			sta->num_dscp_policies--;
 			break;
 		}
 	}
diff --git a/tests/hwsim/test_ap_dscp.py b/tests/hwsim/test_ap_dscp.py
index 56aaa1174..1dcd5f5dd 100644
--- a/tests/hwsim/test_ap_dscp.py
+++ b/tests/hwsim/test_ap_dscp.py
@@ -5,6 +5,7 @@
 # See README for more details.
 
 import hostapd
+import time
 from utils import *
 
 
@@ -237,3 +238,35 @@ def test_ap_dscp_add_send_remove_send(dev, apdev):
     expect_dscp_event_contains(dev[0], "add policy_id=2 dscp=34")
     expect_dscp_event_contains(dev[0], "add policy_id=4 dscp=50")
     expect_dscp_event_contains(dev[0], "request_end")
+
+def test_ap_dscp_rejected_policy_then_query(dev, apdev):
+    """AP DSCP: STA rejects a policy, then sends a wildcard query"""
+
+    hapd = ap_client_connect(dev, apdev, enable_dscp=True)
+    sta = dev[0].own_addr()
+
+    set_ap_dscp_policy(hapd, sta,
+                       "policy_id=1 request_type=Add dscp=46 "
+                       "classifier_mask=0x51 ip_version=4 "
+                       "dst_port=5002 protocol=17")
+    set_ap_dscp_policy(hapd, sta,
+                       "policy_id=2 request_type=Add dscp=34 "
+                       "domain_name=video.wifitest.org")
+    send_unsolicited_dscp_req(hapd, sta, 0, [1, 2])
+    expect_dscp_event_contains(dev[0], "request_start")
+    expect_dscp_event_contains(dev[0], "add policy_id=1")
+    expect_dscp_event_contains(dev[0], "add policy_id=2")
+    expect_dscp_event_contains(dev[0], "request_end")
+
+    if "OK" not in dev[0].request("DSCP_RESP policy_id=1 status=1 policy_id=2 status=0"):
+        raise Exception("DSCP_RESP failed")
+    time.sleep(0.1)
+
+    # The rejected policy is gone; the query gets the other one
+    if "OK" not in dev[0].request("DSCP_QUERY wildcard"):
+        raise Exception("DSCP_QUERY failed")
+    expect_dscp_event_contains(dev[0], "request_start")
+    expect_dscp_event_contains(dev[0], "add policy_id=2")
+    expect_dscp_event_contains(dev[0], "request_end")
+    if "PONG" not in hapd.request("PING"):
+        raise Exception("hostapd did not survive the query")
-- 
2.47.3





More information about the Hostap mailing list