Flint 2 GL-MT6000 4.9.1-op25: gl_sdk4_tertf IPv6 GC/keepalive race causes NS flood and complete Wi-Fi/WAN lockup

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_ns calls per second;

  • the destination contains kernel pointers;

  • the stack moves between send_ns, bridge and mac80211 TX 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

  1. Can you confirm whether this IPv6 GC/keepalive race is already known?

  2. Is a fixed kmod-gl-sdk4-tertf package or firmware build available for the Flint 2?

  3. Can you confirm that disabling gl-tertf/gl_clients and unloading the module is the recommended temporary workaround?

  4. Could the corresponding source for package version 26.091.28257~6daeab1-r1 be 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.

Hello, they know about the issue and they provided an test firmware which fixes the issue.

2 Likes