Hello,
I am reporting a reproducible-looking kernel race in the GL.iNet client tracking module gl_sdk4_tertf on a Flint 2. It caused both Wi-Fi SSIDs to disappear, WAN/Tailscale connectivity to fail, and required a manual power cycle.
I have persistent router logs and an external Suricata sensor, so the failure was captured from its exact start until the power cycle. The evidence strongly points to an IPv6 GC/keepalive race inside gl_sdk4_tertf, not malformed traffic from a client.
Environment
-
Device: GL.iNet Flint 2 / GL-MT6000
-
GL firmware:
4.9.1-op25 -
OpenWrt base:
25.12.5 r33051-f5dae5ece4 -
Kernel:
6.12.94 -
Module package:
kmod-gl-sdk4-tertf-6.12.94.26.091.28257~6daeab1-r1 -
gl-sdk4-clients:26.079.34176~209fb6f-r1 -
Module SHA-256:
79a3790e045d8face8f2b69d943d33d38ca82706b061637a96e8c07d41ab9144 -
TERTF TTL:
30 -
Tracked networks:
br-lan,br-guest,br-iot -
WED: enabled
-
Software/hardware flow offloading: disabled
-
The router has no IPv6 address on
br-lan; its only local IPv6 address is loopback::1.
There is a TC traffic mirror for unicast monitoring, but multicast is explicitly excluded. No TC or act_mirred function appears in the stall stack, the mirror supervisor remained healthy, and the failing packets were visible to the external sensor as ordinary bridge multicast traffic.
User-visible incident
-
Approximately 17:28:16: the kernel stall and packet flood started.
-
17:35–17:36: all Wi-Fi clients disconnected and both SSIDs disappeared.
-
The router also became unreachable through Tailscale/WAN.
-
Ethernet carrier to the external sensor remained up and remote syslog kept arriving until the manual power cycle.
-
Approximately 17:42:30: I manually power-cycled the router.
-
Wi-Fi and WAN recovered after reboot.
The RPi sensor itself did not reboot. Its Ethernet link went down only when the router was power-cycled, so the cable/sensor was not the trigger.
Kernel stall
The router reported the same continuous RCU stall every three minutes. The CPU, PID, grace period and softirq counter stayed unchanged:
Fri Jul 17 17:29:16 2026 kern.err kernel:
rcu: INFO: rcu_sched self-detected stall on CPU
rcu: 2-....: (5999 ticks this GP) softirq=620073/620073 fqs=2691
rcu: (t=6001 jiffies g=1835153 q=9168 ncpus=4)
CPU: 2 PID: 766 Comm: kworker/2:5
Tainted: G O 6.12.94 #0
Tainted: [O]=OOT_MODULE
Workqueue: events_long send_ns [gl_sdk4_tertf]
pc : queued_spin_lock_slowpath+0x78/0x3a0
lr : __ieee80211_schedule_txq+0x970/0x9c0 [mac80211]
Call trace:
queued_spin_lock_slowpath
__ieee80211_schedule_txq [mac80211]
ieee80211_tx_prepare_skb [mac80211]
ieee80211_xmit [mac80211]
__ieee80211_subif_start_xmit [mac80211]
ieee80211_subif_start_xmit [mac80211]
ieee80211_subif_start_xmit_8023 [mac80211]
dev_hard_start_xmit
__dev_queue_xmit
br_dev_queue_push_xmit
br_forward_finish
__br_forward
maybe_deliver
br_flood
br_dev_xmit
dev_hard_start_xmit
__dev_queue_xmit
send_ns+0x378/0x480 [gl_sdk4_tertf]
send_ns+0x428/0x480 [gl_sdk4_tertf]
process_one_work
worker_thread
kthread
ret_from_fork
Later reports showed the same task still stalled:
17:32:16 t=24006 pc=ieee80211_proberesp_get [mac80211]
17:35:16 t=42011 pc=__ieee80211_subif_start_xmit [mac80211]
17:38:16 t=60016 pc=__skb_flow_dissect
17:41:16 t=78021 pc=send_ns+0xf4/0x480 [gl_sdk4_tertf]
The last report was directly inside send_ns:
Workqueue: events_long send_ns [gl_sdk4_tertf]
pc : send_ns+0xf4/0x480 [gl_sdk4_tertf]
lr : send_ns+0xc0/0x480 [gl_sdk4_tertf]
Exact ICMPv6 flow
Suricata retained one flow containing essentially the entire flood:
start: 2026-07-17 17:28:16.038392 CEST
end: 2026-07-17 17:42:30.233037 CEST
protocol/type: IPv6-ICMP / 135 (Neighbor Solicitation)
source: ::1
destination: e03d:1288:c0ff:ffff:687f:0680:c0ff:ffff
packets: 63,162,777
bytes: 5,431,998,822
rate: approximately 73,961 packets/second
Suricata recorded zero capture errors and no increase in kernel drops during the incident.
The destination is not a real client IPv6 address. Interpreting its two 64-bit halves in little-endian order gives:
0xffffffc088123de0
0xffffffc080067f68
These look like kernel virtual addresses. The first one is only 0x40 away from x21=0xffffffc088123da0, which was captured in the final RCU register dump while send_ns was running. This strongly suggests that an already freed or repurposed object was interpreted as an IPv6 address.
Binary analysis of the exact module
The captured module is not stripped, so I inspected the relevant AArch64 disassembly.
The behavior appears equivalent to:
gc_work_ipv6:
lock(term);
list_del_init(expired_ipv6_node); // next/prev point to itself
unlock(term);
synchronize_rcu();
kmem_cache_free(expired_ipv6_node);
keepalive_work_ipv6:
for (node = head->next; node != head; node = node->next)
send_ns(dev, &node->ipv6_address);
The keepalive list traversal does not appear to take the per-term lock used by the GC path, and I cannot see an RCU read-side critical section around that traversal.
If GC removes a node after keepalive has loaded it, the keepalive iterator can remain on the self-linked node forever. Because it is not protected as an RCU reader, the node can then be freed and reused while keepalive still reads it. This explains all observed evidence:
-
one workqueue task never completes;
-
the same malformed NS flow continues until power-off;
-
approximately 74k
send_nscalls per second; -
the destination contains kernel pointers;
-
the stack moves between
send_ns, bridge andmac80211TX functions.
Trigger
The client traffic immediately before the incident was normal link-local IPv6 and mDNS. No client emitted a flood or malformed IPv6 packet.
Three tracked IPv6 records had their last observed packet within the same 0.5-second interval. Their 30-second TTL boundaries were approximately:
17:28:15.429
17:28:15.524
17:28:15.905
The malformed flow started at 17:28:16.038, 133 ms after the latest of these boundaries. Therefore the practical trigger was ordinary expiry of an IPv6 client record while the GC and keepalive workers overlapped. I cannot identify which of the three records was involved because the node contents had already been overwritten.
This does not appear to require Google Meet, Slack, high throughput, or unusual client behavior. Any ordinary IPv6/mDNS client may be able to expose the race when scheduling aligns.
Workaround
After the incident, gl_sdk4_tertf, gl-tertf and gl_clients_update were automatically loaded again, so the risky code path remains active after a normal reboot.
My planned temporary workaround is to stop/disable gl_clients and gl-tertf, disable module autoload, and unload gl_sdk4_tertf. The expected impact is loss of GL.iNet client/live traffic statistics while core routing, DHCP and Wi-Fi continue to operate.
Questions for GL.iNet
-
Can you confirm whether this IPv6 GC/keepalive race is already known?
-
Is a fixed
kmod-gl-sdk4-tertfpackage or firmware build available for the Flint 2? -
Can you confirm that disabling
gl-tertf/gl_clientsand unloading the module is the recommended temporary workaround? -
Could the corresponding source for package version
26.091.28257~6daeab1-r1be made available so the locking/RCU analysis can be verified?
I can provide the complete persistent kernel log, the exact .ko, Suricata stats/events and additional register dumps privately to GL.iNet staff. The full archives contain client identifiers and DNS history, so I do not want to publish them without redaction.