2446 Commits
Author SHA1 Message Date
Nick Hainke 84802285f4 hostapd: pack hostapd-utils and wpa-cli in one variant only
hostapd-utils and wpa-cli use VARIANT:=*, so every variant packs them.
Since the variants are built in parallel, they write the same package
file and pkginfo files at the same time. This makes the build fail
randomly:

  Package hostapd-utils is missing dependencies for the following libraries:
  libgcc_s.so.1

Only pack them in the first selected variant that builds them.

Fixes: b029d56e0b ("hostapd: build the variants in parallel")
Link: https://github.com/openwrt/openwrt/pull/25796
Signed-off-by: Nick Hainke <vincent@systemli.org>
2026-10-11 09:26:39 +02:00
Julius Bairaktaris ad18024862 ppp: build the PPPoA plugin without libatm
The PPPoA plugin only uses text2atm() and text2qos() from libatm. ppp
ships copies of both and uses them unless configured --with-atm, so
link the bundled copies and drop the dependency on linux-atm.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
Link: https://github.com/openwrt/openwrt/pull/25707
Signed-off-by: Robert Marko <robimarko@gmail.com>
2026-10-08 15:23:35 +02:00
Felix Fietkau 665fcb0f05 hostapd: ubus get_status reports the spatial reuse settings
The spatial reuse settings that a radio runs with were not visible at
runtime: get_status did not report them, and nl80211 does not carry
them. A radio with he_spr_sr_control=3 could not be told apart from an
unrestricted one.

get_status now adds, beside the BSS Color fields, at the top level and
in each entry of the links of an AP MLD, the running values of the
radio's configuration:
- he_spr_sr_control, he_spr_non_srg_obss_pd_max_offset,
  he_spr_srg_obss_pd_min_offset and he_spr_srg_obss_pd_max_offset, as
  numbers;
- he_spr_srg_bss_colors and he_spr_srg_partial_bssid, as the bitmap
  value with bit n for colour or partial BSSID n (IEEE Std 802.11-2024
  9.4.2.251), in 16 lowercase hex digits. The element holds the value
  little-endian; a ubus integer is signed, and colour 63 is bit 63.
  he_spr_srg_bss_colors=7 8 reads "0000000000000180".

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:39 +02:00
Felix Fietkau f3371c7625 hostapd: abort a pending interface setup when its config is removed
iface_restart() aborts the pending setup of a phy before it removes the
interface, but iface_config_remove() does not. A config_set without
interfaces, the shutdown and the removal of the last link of an AP MLD
left the setup pending, and its wpa_supplicant reply later added an
interface for a configuration that no longer exists.

Abort the pending setup in iface_config_remove() as well.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:39 +02:00
Felix Fietkau 1e0e1065b0 hostapd: undo a failed AP BSS add in the nl80211 driver
When nl80211_setup_ap() fails in wpa_driver_nl80211_if_add(), the new
i802_bss stays linked into the driver and in *drv_priv. For an AP MLD
BSS, the caller's cleanup then runs driver_nl80211_link_remove() on
that BSS, which reports EVENT_MLD_INTERFACE_FREED with a NULL context,
and hostapd crashes in hostapd_wpa_event(). A failure of
nl80211_set_mode() leaks the BSS.

198-nl80211-undo-a-failed-ap-bss-add undoes the add on both failures,
as the other error paths of the function do. Upstream hostap has the
same code.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:39 +02:00
Felix Fietkau a75b4c405f hostapd: continue a pending interface setup from the event loop
The bss_remove hook of hostapd.uc sends a service event with a
synchronous ubus call. While libubus waits for that reply, it runs the
completion callbacks of other pending requests, among them the
wpa_supplicant phy_status or phy_set_state request of an interface
setup in progress. The callback advanced the setup to
hostapd.add_iface() nested inside the teardown that called the hook.

With an AP MLD, a config_set for radio 0 followed by one for radio 1
added the radio 0 link while the radio 1 link was half removed and
still held its driver BSS and management frame registrations. The add
failed with "Match already configured", and its cleanup crashed hostapd
in hostapd_wpa_event(). This happened on the first such reconfiguration
after a bring-up.

Run the next step of the pending setup from a uloop timer instead, and
cancel the timer where the pending request is aborted.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:39 +02:00
John Crispin 0e1e3a0919 hostapd: ACS scans for all links of an AP MLD with one request
The links of an AP MLD share one wiphy, which runs one scan at a time.
With ACS on several links, each link scans for itself, and every link
after the first gets -EBUSY and retries every 5 seconds.

172-acs-scan-for-all-links-of-an-ap-mld-with-one-request (from John
Crispin) adds the channels of every partner link in ACS to the scan
request, and hands the survey of that scan to each link. The partner
scan check of hostapd_driver_scan() from 171 moves into
hostapd_mld_partner_scanning(). Changes to the patch as received:
- every link, the requesting one included, continues from the event
  loop. The partner fallback of EVENT_SCAN_RESULTS otherwise runs the
  scan callback of a partner whose next scan a link started from its
  own callback, and counts that scan as completed at once;
- every link keeps its scan callback until the survey is fetched, so
  that each link of the scan gets its survey on whichever link the
  driver reports it;
- a link that the scan of a partner link includes waits at most
  ACS_SCAN_WAIT_TIMEOUT seconds for its results and then scans for
  itself, so that it does not stay in ACS for good when the results
  never come, for example when the partner link is torn down during the
  scan;
- acs_cleanup() drops the ACS scan callback, so that a link disabled
  during its scan does not make hostapd_mld_partner_scanning() refuse
  the scans of its partner links.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:39 +02:00
Felix Fietkau a7c1609a28 hostapd: undo BSS Color Disabled if no colour change starts
hostapd_switch_color_timeout_handler() sets he_bss_color_disabled
before it starts the colour change of each BSS of the radio. The
driver installs the CCA beacons, which carry BSS Color Disabled, only
for a change it starts. Where hostapd_drv_switch_color() fails, or the
settings cannot be built, for every BSS, no beacon carries the disabled
state, but the configuration keeps it: get_status reports -1 for a
colour the beacons still use, and a later beacon rebuild disables BSS
Color without a colour change.

607-ap-restore-bss-color-disabled-when-no-color-change-starts counts
the colour changes that started, and where none did, restores the
state BSS Color Disabled had before. That also resumes the ageing of
the OBSS colours, which the disabled state paused (174). The
bss-color-change notification after the loop (606) then reports the
restored state.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:39 +02:00
Felix Fietkau ae3fabae25 hostapd: ubus notifies a change of the BSS Color fields
A subscriber that reports the BSS Color and the OBSS colours of a radio
must know when they change without a configuration change. The kernel announces a colour change that the
driver starts, aborts or completes, but hostapd takes the new colour,
or leaves BSS Color disabled, only when it handles that event. Some
changes have no event at all:
- with no free colour left, the collision timeout disables BSS Color
  and picks a new colour itself;
- a collision timeout whose colour change the driver does not start
  leaves BSS Color disabled;
- the OBSS colours of the radio change.

606-ap-notify-ubus-of-a-bss-color-change calls the new
hostapd_ubus_notify_bss_color():
- in the no free colour branch of the collision timeout;
- after the colour change loop of the collision timeout. This call
  fires in every case, whether the driver started the change or not;
- at the end of hostapd_event_color_change(), after hostapd took the
  new colour at a completion, or left BSS Color disabled at an abort;
- when the aged set of OBSS colours of the radio changes, as a colour
  is added or leaves it (174), and not at each repeated collision
  event.

It sends the bss-color-change notification, { ifname, bssid, link_id }
(link_id for an affiliated AP of an AP MLD), on the object of the BSS.
get_status carries the new values.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:39 +02:00
Felix Fietkau 5fe8fade02 hostapd: ubus get_status adds partial and in-use BSS colours
A subscriber that reports the BSS colours of a radio needs three
fields: the BSS Color subfield and the Partial BSS Color subfield that
the radio transmits, and the colours of the OBSSs that the AP
identified itself or learnt from the BSS Color Collision reports of its
stations. get_status named the
colour, as -1 while BSS Color is disabled, and neither of the others.

hostapd kept its colours in use per BSS, and each collision event
replaced them. It also added the colour of a BSS Color In Use report,
which names a colour a station uses with a peer (IEEE Std 802.11-2024
11.21.2.8), not an OBSS colour. 174-ap-keep-the-obss-colors-of-a-radio
keeps two sets per radio:
- the OBSS colours, which the kernel's collision events and the BSS
  Color Collision reports of stations add to. A colour leaves the set
  60 s after an event named it, or 130 s after a report did (the
  longest dot11BSSColorCollisionSTAPeriod, 120 s, plus a 10 s margin),
  and the ageing pauses while BSS Color is disabled;
- the colours of BSS Color In Use reports, held per station until a
  report of colour 0 or until the station leaves.
A colour change picks a colour outside both sets.

get_status now adds, beside bss_color, at the top level and in each
entry of the links of an AP MLD:
- he_bss_color: the BSS Color subfield of the HE Operation element.
  It keeps the colour while BSS Color is disabled
  (IEEE Std 802.11-2024 26.11.4); bss_color keeps -1 then;
- bss_color_partial: he_bss_color_partial of the radio;
- bss_colors_in_use: the OBSS colours of the radio, 1 to 63. The
  colours of BSS Color In Use reports are not among them.

On mt76 the kernel's collision events name the colliding colour only:
mt7996 does not detect collisions itself, and mac80211
(ieee80211_rx_check_bss_color_collision()) reports an OBSS whose colour
equals the BSS's own. Other OBSS colours reach the set only through
the BSS Color Collision reports of stations.

The colours are a list rather than the 64-bit bitmap, because a blobmsg
int64 with bit 63 set reads as a negative number in ucode and JSON.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:39 +02:00
Felix Fietkau 02f3f670f2 hostapd: ubus get_status lists the links of an AP MLD
The links of an AP MLD share one ubus object, and get_status describes
the BSS of one link (mld->fbss, or the first link that joined the
object). Each affiliated AP runs on its own radio, with its own
hostapd_iface and so its own channel and BSS Color. A subscriber could
read the BSS Color of one radio of an AP MLD only, not the BSS Color
that each radio transmits.

For an AP MLD, get_status now adds links [{ link_id, bssid, freq,
bss_color }], one entry per link that joined the object: the link ID,
the BSSID of the affiliated AP, its frequency, and its BSS Color with
the meaning of the top-level field (-1 while BSS Color is disabled).
The top-level fields are unchanged. A BSS that is not affiliated with
an AP MLD gets no new field.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:39 +02:00
Felix Fietkau 0772ebc5ae hostapd: ubus key-mismatch names the BSSID of its link
The links of an AP MLD share one ubus object, and the ifname of the
key-mismatch event is the MLD netdev. A subscriber could only take the
BSSID of get_status, the BSSID of the first BSS of the MLD, whichever
link the station used. A subscriber that reports a failed connection
needs the BSSID of the BSS that the station tried to connect to, so it
named the wrong BSS for an attempt on another link.

hostapd_ubus_notify_key_mismatch() now adds:
- bssid: own_addr of the hostapd_data that handles the failure, the
  BSSID of the link for an affiliated AP, as it is: the link of the
  SAE Authentication frame, or the association link for the 4-way
  handshake;
- link_id: the link ID of that affiliated AP, for an AP MLD only.

The 4-way PSK failure of hostapd_wpa_auth_psk_failure_report() (an
invalid MIC in EAPOL message 2) now goes through the same function, so
its event carries the two fields too. It carries no rsn_error:
HOSTAPD_UBUS_RSN_ERROR_NONE (0, a reserved value) leaves the field out.
RSN Error 3 (EAPOL message 2) belongs to a separate patch that changes
the psk_failure_report callback to pass the EAPOL-Key frame. Until that
patch lands, the event of a PSK mismatch on an RSNA BSS carries no
rsn_error. The SAE Confirm event does not change otherwise.

The two lines the call takes in 600 move the wpa_auth_glue.c hunks of
601 and 730 by one; refresh them so that the series applies without
offsets.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:39 +02:00
Felix Fietkau b4072f459d wpa_supplicant: keep the address of a renamed station MLD
A new station MLD took over the address of a removed one only where
the names were the same. netifd numbers the MLO interfaces in the order
of their sections, so a new section renames a station MLD (sta-mld0 to
sta-mld1), and the station MLD got a new address. Other devices then
take it for a new station MLD, although it continues the existing one.

mld_set_config() now uses the rule of the AP MLD (mld_prev_match()):
on one wiphy, the same SSID and radios, then the same SSID. For a
continued station MLD with another name, the MLD sync names the address
of the old one; the entry of the old name loses it, and the new name
claims it. A station MLD that continues none asks by its name, as
before.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 54535d1ed8 wifi-scripts, hostapd: keep the address of a renamed AP MLD
netifd numbers the MLO interfaces in the order of their sections
(mlo_vif_create()), so the name of an AP MLD is no identity: a new
section that sorts first renames the MLDs after it (ap-mld0 to ap-mld1).
hostapd took the renamed MLD for a new one and gave it a new address.
Its links got new addresses too, although the configuration of the MLD
did not change.

- mld_prev_match() in common.uc decides which removed MLD a new MLD
  continues. On one wiphy, two passes in order: the same SSID and
  radios, then the same SSID. The first pass separates two MLDs with one
  SSID. A new MLD that matches none asks netifd by its name, as before:
  it gets the address that its name had, unless another MLD continues
  that one, and else a new address, as a new BSS does.
- The MLD sync names the address of a continued MLD with another name
  for the new name. The entry of the old name loses the address to it,
  so a new MLD with the old name finds no entry.
- The links of a removed MLD release their entries on every radio that
  hostapd has a configuration for (macaddr_release). The reload of
  mld_set_config() reaches only the links that run, so a link whose BSS
  failed or was pending kept its entry, which shares the address of the
  old MLD.
- The entry of a link of a continued MLD moves to the link of the new
  MLD on that radio (macaddr_release with to), where that name has no
  live entry. A stale entry on the target name is a leftover of an
  earlier removal, unless the target is the old name of another MLD of
  the same change; hostapd lets the move replace it only in the first
  case (replace). Without that, the leftover kept the target name: the
  continued MLD got the old link address of that removed MLD, and an
  unrelated new MLD took the link address of the continued one.
  In a chain of renames, the MLD whose new name is free moves
  first and frees its old name for the next one. A cycle (two MLDs that
  swap their names) has no free name: the entries stay stale with their
  old names, and the links of the new MLDs claim them by name. A link
  that carries the MLD address follows the MLD; each other link keeps
  the address that its name had, not the one of the link it continues.

Each move happens within one netifd. The sync and the releases go out
before the requests of the new MLDs, and netifd takes the requests of
one connection in order.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau eb8cbaeebd wifi-scripts, hostapd: take wifi MAC addresses from netifd
hostapd, wpa_supplicant and wdev.uc now request the address of each
interface from netifd (network.wireless macaddr_get) and tell it which
names they keep (macaddr_sync). The used lists that they built from
their own configuration, from the lists of each other and from the
netdevs are gone, and with them the ubus methods that passed the lists:
config_get_macaddr_list of hostapd and wpa_supplicant,
wpas_set_macaddr_list of hostapd and phy_set_macaddr_list of
wpa_supplicant. The allocator of common.uc and the get_macaddr command
of wdev.uc are gone too.

Names:
- hostapd: <phy>/<ifname> per BSS, in the group of the PHY
  configuration, and the AP MLD name in the group "mld". A link of an
  AP MLD shares the address of the MLD where the configuration names
  it as its BSSID; a link on radio 0 also follows a generated MLD
  address.
- wpa_supplicant: the ifname, in the group of the PHY configuration,
  and the station MLD name in the group "mld". A station MLD keeps its
  address when its radios change.
- wdev.uc: the ifname, in the group of the radio.

A name keeps its address, so a reconfigured station, mesh or IBSS
interface keeps the address that its peers know. An empty configuration
of a radio releases the entries of its owner for that radio: in
hostapd (iface_set_config(), also when iface_check_mld() takes the last
BSS), in wpa_supplicant (set_config()) and in wdev.uc. An expired link
of an AP MLD (mld_link_bss_remove()) releases its entry too. netifd
keeps the released entries as stale entries, so the interfaces that
come back get their addresses by name, and a configured address takes
them.

A reload of hostapd moves the address of a reused BSS to its new name
in one macaddr_sync, so that a rename or a swap of names keeps each
address with its BSS. A reused first BSS of a fullmac device whose kind
of address changed needs a restart, since its netdev keeps its address.

A configured BSSID that another interface holds is refused, and the
interface does not start.

If netifd cannot be reached, each consumer uses the address that the
name had before, where it knows one, and a hostapd reload keeps the
addresses of the reused BSSes.

The configuration files carry no placeholder BSSID for an interface
without a configured address: hostapd replaced it and wpa_supplicant
and wdev.uc dropped it. hostapd keeps the BSSIDs of an unchanged BSS
list by the name of each BSS instead.

The allocation table lives in netifd only, so a new netifd has no
record of the addresses that the previous one gave. /etc/init.d/network
restart runs wifi down before netifd stops, but a crash of netifd tears
nothing down: the new netifd found its radios running, none of the
consumers asked for an address, and a new interface could get one of
the running addresses. After wifi down too, hostapd kept the data of an
AP MLD whose configuration did not change, and the netdev of that MLD
took its address from that data. wireless.uc therefore resets the
radios when netifd loads it, before config_init() sends mld_set:
- hostapd config_reset,
- wpa_supplicant config_reset, a new method: an empty configuration for
  each radio, and an empty MLD configuration, as hostapd does,
- wdev.uc reset, a new command, for each /var/run/wdev-<phy>.json. It
  removes the interfaces that the state file names and that this radio
  created (drop_inactive()), and writes empty state and id files. It
  opens no ubus connection, as network.wireless does not exist yet at
  this point.

netifd waits for each of these. The two calls use a connection of their
own with a timeout of 3 s, not the global connection of netifd with
30 s, so a hostapd or wpa_supplicant that does not answer holds netifd
start for at most 3 s each. A result other than "not found" goes to the
log. netifd loads wireless.uc once, in netifd_ucode_init() from main(),
so the reset runs only when netifd starts, not at a wifi reload, a
network reload or a reconf.

The cost: a crash of netifd that procd answers with a respawn, and
"ubus call network restart", now restart all wifi, as
/etc/init.d/network restart already did through wifi down. Outside the
ETSI region, cfg80211 sets an available DFS channel back to usable 2 s
after no interface operates on it (cfg80211_dfs_channels_update_work()),
so a radio on a DFS channel runs a new CAC, and the connection of a
backhaul station drops.

Other parts:
- wdev_create() refuses a netdev other than a monitor without an
  address, and hostapd creates its first BSS with its BSSID: without an
  address, mac80211 takes the first free entry of the wiphy address
  list, which netifd can have given to another interface.
- A failed allocation in iface_restart() starts no BSS of the radio and
  restarts wpa_supplicant there, as the other early returns do.
- On a fullmac wiphy, wdev_create() reuses a netdev that is down, and
  nl80211_set_interface() does not read NL80211_ATTR_MAC. The reused
  netdev now gets the requested address through RTM_SETLINK. A driver
  without ndo_set_mac_address, such as ath6kl, keeps the old address,
  with a warning.
- iface_reload_config() checks the fullmac property of the wiphy. It
  passed the configuration name, such as phy1.0, which has no sysfs
  entry, so phy_is_fullmac() checked wiphy 0.
- wpa_supplicant reports the generated addresses in its status method.
- wdev.uc takes num_global=<n> and macaddr_base=<addr> among its
  arguments again.
- wpad_acl.json allows the calls to network.wireless, and the
  postinst of hostapd-common makes ubusd read the ACLs again.

hostapd.uc and wpa_supplicant.uc import common.uc of wifi-scripts, so
both packages must be updated together.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 322b378a16 hostapd: pass the space left in the BSS TM Request buffer to mbo_add_ie()
wnm_send_bss_tm_req() passes buf + sizeof(buf) - pos as the space left
for the MBO element, with buf a pointer. On a 64-bit target sizeof(buf)
is 8 and pos is at least buf + 31, so the value converts to a size_t
near SIZE_MAX and the check in mbo_add_ie() never fails. The allocation,
os_zalloc(1000 + nei_rep_len + mbo_len), leaves room for at most 299
octets of fixed content and the 6 octet MBO element header, so no write
goes past it.

Upstream hostap has the same line on main, pending, main-check, pqc and
eppke as of today, and no commit changes it, so 072 is a local patch
that passes the space left in the allocation.

The two lines that 072 adds move the last hunk of 590 by two; refresh
it so that the series applies without offsets.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 47f3de3540 hostapd: ubus bss_transition_request frees its neighbour report buffer
hostapd_bss_tr_send() allocates nr for the neighbour reports and freed
it only where hexstr2bin() failed. wnm_send_bss_tm_req() copies the
buffer and does not take it over, so every request with neighbors leaked
it, and so did the returns of the MBO checks after the allocation.

The MBO checks and attributes need nothing from nr, so they now run
before the allocation, and nr is freed after wnm_send_bss_tm_req(),
whatever it returns. The one return after the allocation that remains,
a failure of hexstr2bin(), frees nr with os_free(), which matches
os_zalloc().

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau fa76a5c342 hostapd: ubus bss_transition_request refuses values its fields cannot hold
hostapd_bss_transition_request() read each value as a u32 and then
narrowed it without a check. reassoc_delay, mbo_reason and cell_pref were
u8 locals, and hostapd_bss_tr_send() took da_timer as u16 and
valid_period and dialog_token as u8. A value that did not fit lost its
high bits:
- an Association Retry Delay of 256 to 65535 went out as its low octet,
  although the attribute holds two octets, and the check against 65535
  could never be true;
- a Transition Reason Code of 256 became 0 and passed the check against
  MBO_TRANSITION_REASON_PREMIUM_AP;
- the same applied to the Cellular Data Connection Preference, the
  Validity Interval, the Dialog Token and the Disassociation Timer.

The request values now stay u32, and the handler answers
UBUS_STATUS_INVALID_ARGUMENT for a value larger than its field:
- Dialog Token and Validity Interval: one octet; Disassociation Timer:
  two octets (IEEE Std 802.11-2024, 9.6.13.9, Figure 9-1275);
- Transition Reason Code and Cellular Data Connection Preference: one
  octet (MBO v0.0_r19, 4.2.6 and 4.2.5, as ieee802_11_defs.h names
  them); Association Retry Delay: two octets, as hostapd_bss_tr_send()
  writes it. hostapd's MBO code names no section for that attribute.

hostapd_bss_tr_send() takes reassoc_delay as u16 and keeps only the
check that a delay needs Disassociation Imminent.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 372c27c095 hostapd: drop the unused locals of the ubus code
These locals are never read under any configuration:
- hostapd_notify_response(): elems, pos, len;
- hostapd_bss_wps_status(): rc;
- hostapd_rrm_print_nr(): data;
- hostapd_bss_mgmt_enable(): cur, neigh, beacon;
- hostapd_rrm_beacon_req(): mode, which was assigned only; the frame
  reads the Measurement Mode from the request attribute;
- hostapd_bss_transition_request(): sta;
- hostapd_wired_get_clients(): sta_driver_data, r;
- hostapd_ubus_notify_bss_transition_response(): i;
- hostapd_ubus_notify_bss_transition_query(): cl_str, i.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 3d9ccda640 hostapd: ubus del_client without a reason sends reason code 1
hostapd_bss_del_client() sets reason only when the request carries
"reason". A request without it passed an uninitialised int to
hostapd_drv_sta_deauth() or hostapd_drv_sta_disassoc() and to
ap_sta_deauthenticate() or ap_sta_disassociate(). The station then got
whatever value the stack held as the Reason Code of the Deauthentication
or Disassociation frame.

Start reason at WLAN_REASON_UNSPECIFIED (1). Reason code 0 is reserved
(IEEE Std 802.11-2024, Table 9-79).

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau fb63781b5a hostapd: ubus key-mismatch carries the RSN Error and the frame body
A subscriber that reports a failed connection on an RSNA BSS needs the
RSN Error, which names the frame in which the RSN procedure failed, and
the body of the last frame that the station sent. The key-mismatch
event carried the station address and the interface name only, so a
subscriber could fill in neither field.

For a mismatching SAE Confirm in handle_auth_sae(), the event now also
carries:
- rsn_error: 1 (Authentication frame);
- frame: the Authentication frame body, from the Authentication
  Algorithm Number field to the end of the frame, as lowercase hex.

The event name and the address and ifname fields do not change, so a
subscriber that reads only those keeps working. For a non-AP MLD the
address is the STA MLD address: handle_auth() looks up or adds the
station by the MLD address of the Basic Multi-Link element.

The key-mismatch event of hostapd_wpa_auth_psk_failure_report() (EAPOL
message 2) does not change.

The hex encoding of a blobmsg field moves into blobmsg_add_hex(), which
the action-frame event and the neighbour report output use too.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau febc7d9b7e hostapd: ubus names the links of a client MLD and their changes
A subscriber that reports each affiliated STA of an associated client
MLD needs the STA's link address and the BSSID of the affiliated AP of
its setup link. hostapd's ubus named the MLD address only, and the
kernel's station dump names neither, so a subscriber reported the MLD
address as the affiliated STA, and could not map the link of a backhaul
STA to its AP link.

For a client MLD, get_clients and sta-authorized now carry
ap_mld_address, assoc_link_id and links [{ link_id, address, bssid }],
one entry per accepted link, read from the station of the association
link. Stations that are not an MLD get no new field.

A new sta-links-changed event carries the same fields where the links
change without a new association:
- 604: when a Link Reconfiguration Response that accepted an addition or
  a removal is acknowledged;
- 605: when an affiliated AP leaves the AP MLD and its link stations are
  freed.

197 frees a link station through one helper, ap_free_link_sta(), which
also clears the link on the partner stations and updates the state
machine's link information. It undoes the links a request added on every
path that refuses or does not complete it, where before the partner
stations kept them as set up. 604 depends on 197, and 605 builds on
both.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau be43abb733 wpa_supplicant: drop two unused locals of mld_set_config()
Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 6111b889f8 wifi-scripts, wpa_supplicant: a reload keeps the control socket
The generated station file named no ctrl_interface; the socket path came
from add_iface alone. RECONFIGURE then read a configuration without it,
deinitialised the control interface and set up none, so the reload method
left /var/run/wpa_supplicant/<ifname> gone. The file now names the same
directory that add_iface passes, which keeps the socket, and the reload
method applies a station MLD's freq_list again.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau f9c2013aed wpa_supplicant: keep a station's association on a credential-only change
A station whose configuration changed only in its credentials was removed
and added again, or with RECONFIGURE deauthenticated, although the new
settings could still allow the association in use. For example, a change
from PSK and SAE to SAE only, while the station is associated with SAE,
made the station associate a second time.

The iface method network_update() reads the regenerated file with
wpa_config_read() and, where its one network is the current one and the
association in use (SSID, AKM, protocol, ciphers, PMF, the effective
password and SAE PWE) still fits it, moves the credential fields into the
current network without the PMKSA flush that SET_NETWORK does. Otherwise
it refuses, and the caller removes and adds the interface as before.
wpa_supplicant.uc takes this path for a plain station and a station MLD
whose configuration differs only in the credential lines, and applies the
MLD's freq_list again after it.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 8be9eb94da hostapd: keep the configured BSSID of the first link of an AP MLD
601-ucode_support.patch adds the first link of an AP MLD on the
existing netdev with no address, and hostapd_setup_bss() then takes the
netdev address, the MLD address, as the link address. A link on a
radio other than radio 0, whose configured BSSID hostapd.uc keeps apart
from the MLD address (bss_mld_addr()), beaconed with the MLD address,
while ubus get_status reported the configured BSSID. A subscriber that
built its BSS Transition candidates from get_status named a BSSID that
nothing transmitted.

Add a patch that keeps the configured BSSID as the link address and
takes the netdev address as the MLD address, as hostapd_driver_init()
does for the first interface. Without a configured BSSID, the link
takes a random address with the OUI of the netdev, as
hostapd_driver_init() does (pre_setup_mld), so that the link address
cannot be the MLD address: a later link that is given the MLD address
as its BSSID would fail ADD_LINK with -EALREADY. hostapd.uc always sets
a BSSID, so OpenWrt configurations take the configured address.

Have get_status and the channel-switch event report hapd->own_addr,
the address the BSS transmits with, instead of conf->bssid, which is
all zeros where the configuration names none.

Fixes: 816c2d86e7 ("hostapd: add support for MLO interfaces in ucode")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau c7caee422b hostapd: keep the Beacon events of an AP MLD while a link beacons
The nl80211 driver gave up the wiphy's Beacon event subscription
whenever one link stopped, and the other links of an AP MLD lost the
OBSS Beacon events that feed OLBC detection and the AP list. Add a
patch that gives it up with the last link.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 847638cfdd hostapd: remove an affiliated AP at the expiry of its timer
Patch 186 counts the AP Removal Timer and left the BSS running at 0, so
the removed link kept beaconing until the caller's configuration
removed it, 1.64 s late on MT7996. Add a patch that stops the
affiliated AP at the expiry of its timer and hands the BSS to the ucode
bss_link_removed hook.

hostapd knows the TBTT to one beacon interval only: nl80211 reports
neither a Beacon transmission nor the TSF of an AP. The count runs from
the return of the first Beacon update with the count, and the link
stops after the Beacon with the count 1 and before the TBTT the count
names, so no Beacon carries the reserved value 0.

At the expiry, the other links drop the Reconfiguration element. Each
station associated over the link gets a Disassociation, or a
Deauthentication on a remaining link where it is a non-AP MLD, and the
link waits for their TX status, for at most a quarter of a beacon
interval. The patch message lists the limits of the timing. A link in
an MBSSID set with other BSSes is refused, as mac80211 stops the whole
set with it.

The bss_link_removed hook removes the BSS the way a configuration
without it would: bss.delete(), or the removal of the PHY's interface
where it was the only BSS. It has no fallback to a PHY restart, which
would take the PHY's other BSSes down. The configuration that follows
then finds nothing to do. The hook also takes the radio of the link out
of the MLD's configuration, so that a configuration that brings the
link back creates the MLD netdev again, as mld_config_matches() does
for any radio that joins.

The patch also refuses to announce the removal of a link where no other
link of the AP MLD beacons, and checks again at the expiry, as the other
links can stop during the countdown. IEEE 802.11be-2024 35.3.6.3
removes an affiliated AP from an AP MLD that goes on. Such a link keeps
running with its Beacon rewritten without the Reconfiguration element,
and the caller's configuration decides whether it goes. The
mld_link_remove ubus method answers a refusal with
UBUS_STATUS_NOT_SUPPORTED, and no event reports a link kept at the
expiry.

The mld_link_remove ubus method now answers expiry_ms, the time until
the expiry, rounded down, and removal_ms, the time until the link stops
at the latest, with that wait, rounded up.

A BSS of an AP MLD that goes through bss.delete() or remove_iface
without the timer now has the Beacons of the links that stay rewritten,
as they named it in their Reduced Neighbor Report, the number of
simultaneous links and a Reconfiguration element whose count stopped.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau d1359757c6 hostapd: re-create an AP MLD whose SSID changes
All links of an AP MLD carry one SSID. With cfg80211 keeping the SSID of
an interface while another link beacons, nl80211_start_ap() refuses a
link whose SSID differs from it ("require identical SSID for MLO"). An
SSID change applied PHY by PHY then leaves the first PHY's link down:
its new SSID is refused while the other links still beacon with the
old one.

Treat an SSID change as a new AP MLD in mld_config_matches(), so every
link stops before any starts with the new SSID.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau d8bc86e111 hostapd: remove an affiliated AP without re-creating the AP MLD
mld_config_matches() kept an MLD netdev only where its radio mask was
unchanged. An AP MLD that lost one affiliated AP was therefore removed
and created again, and every link stopped beaconing while it was.

Keep the MLD where its radios are a subset of the radios it runs now.
The removed radio's configuration drops the BSS, and bss.delete() takes
the link off the netdev (NL80211_CMD_REMOVE_LINK) while the other links
keep running. The netdev keeps the radio mask it was created with,
which cannot change while it is up, and which may name a radio without
a link. A radio that joins, the one of a removed link included, still
gets a new netdev: mt7996 keeps the state of a removed link until the
netdev goes, and adding the link again beside it is not known to work.

uc_hostapd_bss_delete() frees the link stations of an MLD BSS before its
STOP_AP, which flushes every station that holds the link: a non-AP MLD
that also holds another link keeps its association only where its link
station goes first.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 137921ab08 hostapd: ubus: give an AP MLD one object that outlives its links
The links of an AP MLD share one interface name. Each asked for the
object of that name, the first link got it, and the others failed. The
object, its subscribers, and the state a subscriber set through it
belonged to that one link:

- the object went away with that link, although other links remained,
  and a subscriber took the removal for the end of the AP MLD and of
  every station on it;
- notify_response, the BSS Transition Management Query answer flag and
  the bans applied to that link only, while each link read its own;
- bss_mgmt_enable and the rrm_nr methods changed the capabilities and
  the neighbor report database of that link only.

Keep one object per AP MLD, from its first link to its last. A method
runs on a link that is there: the AP MLD's first link once its setup
has added it to the object, or, once the last link has left the AP MLD
and before its BSS frees the object, a link the object still counts. A
link is counted once. Notifications of every link go through it.
notify_response, the BTM query answer and the bans are the AP MLD's,
and every link reads them. bss_mgmt_enable, the implicit neighbor
report enable of the rrm_nr methods and rrm_nr_set apply to every
link, and a link that joins later takes the flags and the neighbor list
set before.

The methods that name a station act on the link that holds it:
get_clients lists the stations of every link, a non-AP MLD once with
the station of its association link, and rrm_beacon_req, rrm_lm_req
and update_airtime find the station on any link, as del_client and
bss_transition_request already did. They answered for the first link
only, and after that link left the AP MLD, for another one.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau db3c06f8b3 hostapd: free the partner link stations of a BSS that is torn down
The teardown of a BSS of an AP MLD freed the stations of that BSS and
left the partner link stations of each non-AP MLD associated over it
on the other links, with no state machine. Add a patch that frees them
in hostapd_bss_deinit_no_free(), which every teardown path of a BSS
runs.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau afac7f62d7 hostapd: free a link station of a removed link without a disconnection
A link that leaves the AP MLD frees the link stations of the non-AP
MLDs associated over another link, for example patch 186 at the count
of 0. Each one reported AP-STA-DISCONNECTED and a ubus "disassoc" event
with the MLD address, although the non-AP MLD stays associated over its
other links, and a subscriber took the event for the station's
departure. Add a patch that frees such a link station without the
event.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 8c061f4b3b hostapd: send a Disassociation on the link of its BSS
hostapd_drv_sta_disassoc() gave a link ID only for a non-AP MLD. The
Disassociation to any other station of an AP MLD went out with the
frequency of the interface's first link and the link address of its
BSS, and where that BSS was not the first link, mac80211 refused the
frame with -EBUSY. Add a patch that passes the link ID for every station
of an AP MLD, as hostapd_drv_sta_deauth() does.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau dd9ba2738e hostapd: ubus: drop the bans of a BSS that goes away
A ban set through del_client or the ban call arms a timeout that names
the BSS, and hostapd_ubus_free_bss() neither cancelled it nor freed the
ban. A BSS freed while a ban was pending left a timeout that deleted
from the ban list of freed memory. The name and the bans also leaked
where the ubus context was already gone.

Name the ban list in the timeout rather than the BSS, and drop every
ban of a BSS when it is freed, whether the context is still there or
not.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 31959b4a5a hostapd: ubus: test the RRM capability bits of bss_mgmt_enable as a set
__hostapd_bss_mgmt_enable_f() tested
bss->radio_measurements[0] & flags == flags, which C parses as
bss->radio_measurements[0] & (flags == flags), that is, the Link
Measurement bit (bit 0).

For Beacon Report, the test read the wrong bit: Beacon Report was left
off wherever Link Measurement was already on, and was reported as
changed where its bits were already set and Link Measurement was off.
For Link Measurement, whose flag is bit 0, the test gave the right
answer.

Compare the masked bits with the set in both cases.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 06716c2545 hostapd: report the interface state of each BSS in status
`status` says whether a BSS runs, but not why one does not. A BSS in
HT_SCAN starts within seconds, one in DFS waits for the channel
availability check, and one that is DISABLED may not start at all. A
caller that waits for BSSs to come up needs to tell these apart.

Add `state` to each link and each non-MLO entry, as iface.state() names
it: UNINITIALIZED, DISABLED, COUNTRY_UPDATE, ACS, HT_SCAN, DFS, NO_IR or
ENABLED.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 84870d2e15 hostapd: drop unused variables from the ucode bindings
Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau e3c9c43ec1 hostapd: keep the monitors of a control socket across bss.rename()
bss.rename() closed the control socket of the BSS and opened a new one
under the new name. This dropped every attached monitor, for example
hostapd_cli -a or a wpa_cli in interactive mode. Keeping the list of
monitors does not help: the socket of a monitor is connected to the old
control socket, and the kernel refuses datagrams to it from any other
socket (unix_may_send()).

Keep the socket and give it the new name: link() the new path to the
socket file, then unlink() the old one. connect() finds a socket by
the inode of its path, so new clients reach the same socket. link()
does not replace an existing file, so a name that is in use stays
intact. If the move fails, for example on a leftover file of that
name, close the socket and open a new one as before.

The ucode code needs the path of the socket, which only
hostapd/ctrl_iface.c builds. Export it through hapd_interfaces, as
ctrl_iface_recv is.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:38 +02:00
Felix Fietkau 48413952f9 hostapd: report a control socket failure of bss.rename()
bss.rename() ignored the results of hostapd_set_ctrl_sock_iface() and
ctrl_iface_init(). A BSS that could not open the control socket under
its new name was reported as renamed, and ran on without a control
socket.

Return null in that case, as add_bss() does when ctrl_iface_init()
fails. hostapd.uc then restarts the phy, which opens the control
sockets again.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:37 +02:00
Felix Fietkau 237147b82f hostapd: rename the netdev of a BSS before its ubus object goes
bss.rename() freed the ubus object and closed the control socket before
it renamed the netdev. If the driver rename failed, it only opened the
control socket again, and the BSS stayed without its ubus object.

Rename the netdev first. Nothing else depends on its name, and a
failure now leaves the BSS as it was.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:37 +02:00
Felix Fietkau 764a1c0b7d unetd: derive the DHT id from board-base.json
/etc/board.json contains runtime wlan data, which can differ with the
time of wiphy detection. board-base.json holds only the board.d data.
The DHT id changes once after the upgrade.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:37 +02:00
Felix Fietkau 4fc59a8b8c hostapd: announce the removal of an affiliated AP of an AP MLD
An AP MLD has to announce the removal of an affiliated AP in its Beacon
and Probe Response frames before it removes the AP (IEEE 802.11be
35.3.6.3). hostapd builds the Reconfiguration Multi-Link element only
with CONFIG_TESTING_OPTIONS, and no ubus method reaches it, so a caller
that drops a link of an AP MLD cannot announce it.

Patch 186 builds the link removal without CONFIG_TESTING_OPTIONS, takes
the links of the element from the AP MLD, sets the Critical Update
flag, and at the count of 0 leaves the BSS running instead of disabling
the whole radio.

Add bss.link_remove(count) to the ucode API and mld_link_remove
{ phy, radio, iface, count } to the hostapd object. The reply gives
removal_ms, the time until the count reaches 0; a repeated call keeps
the running count. A count outside 1 to 65535 is an invalid argument.
The caller then removes the affiliated AP by reconfiguring it.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:37 +02:00
Felix Fietkau 86d3d90753 hostapd: answer a frame from an unknown station on its own link
The class 3 deauth went out on the link whose address equals the AP MLD
address, where a station on another link never takes it.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:37 +02:00
Felix Fietkau 529f60df8e hostapd: unlink a BSS from its AP MLD when ucode add_bss fails
The error path of iface.add_bss() freed an AP MLD link that
hostapd_setup_bss() had already added to mld->links, without the MLD
reference it took. The next link to join that MLD wrote through the
freed node, which crashed hostapd after "Failed to add new bss ap-mld0
on phy phy0.0" and the restart of phy0.0 that follows it. The error
path now unlinks the BSS, drops the reference and frees an MLD left
unused, as hostapd_add_iface() and bss.delete() do.

Patch 182 makes hostapd_mld_remove_link() skip a node that is on no
list, which the new unlink reaches for a setup that fails before the
link joins.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:37 +02:00
Felix Fietkau 897296cd31 hostapd: pass a Beacon Report without a Measurement Report field to ubus
A Measurement Report element of a Beacon report can come without its
Measurement Report field: from a station that refuses the request or
cannot perform it, with the Refused or Incapable bit set (802.11-2024
9.4.2.20.1), and as the empty Beacon report of a station that has no
beacon information for the request, with no bit set (11.10.9.1.1). An
AX210 class station sends the latter for a Passive request on a channel
with no AP. hostapd_handle_beacon_report() passed only reports that
carry the field to hostapd_ubus_notify_beacon_report(), which dropped a
report without one, so such an answer never reached ubus.

Such a report now goes out on ubus with its address, tokens, interface
and rep-mode, and without the fields of the Measurement Report.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:37 +02:00
Felix Fietkau a1270a5073 wpa_supplicant: move the APs of a radio when a station link switches
When the AP a station MLD is connected to switches the channel of one
link, wpa_supplicant receives EVENT_LINK_CH_SWITCH_STARTED rather than
EVENT_CH_SWITCH_STARTED. wpas_ucode_event() passed only the latter to
ucode, so hostapd was never told to move the APs on the radio of that
link. mac80211 then could not finalize the shared channel context
("some interfaces aren't switching") and dropped the station link.

Pass the link event with the same fields plus link_id, and handle it
like the interface event; iface_channel_switch() already maps the
frequency to the radio of an MLD.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:37 +02:00
Felix Fietkau bf0b333927 hostapd: list the elements an AP MLD link profile does not carry
A per-STA profile in the Basic Multi-Link element of an (Re)Association
Response inherits each element of the frame that carries the Basic
Multi-Link element, unless a Non-Inheritance element lists it (IEEE
802.11be 35.3.3.5.1). ieee80211_ml_build_assoc_resp() adds no such
element. When the station associates on a 5 GHz link, the profile of a
2.4 GHz partner link omits VHT Capabilities and VHT Operation and so
inherits those of the 5 GHz link. mac80211 with IEEE80211_HW_STRICT then
rejects the association ("[link 0] VHT capabilities mismatch"), and a
driver without it sets up the 2.4 GHz link with VHT parameters the AP
does not use there.

List HT Capabilities, HT Operation, VHT Capabilities, VHT Operation and
HE 6 GHz Band Capabilities in a Non-Inheritance element where the
carrying frame has them and the link does not, as a Non-Inheritance
element names only elements of that frame (9.4.2.239). A Link
Reconfiguration Response carries none of them outside the Basic
Multi-Link element, so its profiles get no Non-Inheritance element.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:37 +02:00
Felix Fietkau d9c461b73b hostapd: clamp the DPP chirp wait time to the driver limit
wpas_dpp_chirp_start() sends each Presence Announcement with a wait time
of 2000 ms. cfg80211 refuses a frame whose wait exceeds the driver's
maximum remain-on-channel duration with -EINVAL (nl80211_tx_mgmt()), so
on a driver with a smaller maximum, such as mac80211_hwsim with 1000 ms,
no chirp is ever transmitted and the enrollee cannot be found.

Clamp the wait time to max_remain_on_chan, as the other DPP transmit
paths in this file do.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:37 +02:00
Felix Fietkau 178994ca82 hostapd: keep the first link when a later AP MLD link fails
A later link of an AP MLD takes the driver instance of the first link
(hapd->drv_priv = h_hapd->drv_priv) in hostapd_driver_init() and joins
the MLD only after hostapd_drv_link_add() succeeds. When that call, or
the check for driver MLO support, fails, the link returns -1 with the
borrowed pointer still set.

The caller's cleanup, hostapd_deinit_driver(), then treats the instance
as its own: hostapd_cleanup_driver() removes the link with this BSS's
link ID from the shared instance, which is the link of another BSS when
the link IDs collide, or deinits the instance when it looks unshared.
The first link keeps a pointer to a torn down or freed instance, and the
next link that is set up calls get_capa() on it and crashes hostapd.

Seen with an AP MLD whose links span two wiphys: the link on the second
wiphy was assigned link ID 0, hostapd_drv_link_add() refused it, and the
cleanup removed link 0 of the first wiphy.

A link that never joined the MLD now drops the borrowed instance before
it returns, so the cleanup leaves the first link alone.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2026-10-08 09:50:37 +02:00