Technical Root Cause Analysis
1. Involved Files, Functions, and Data Structures
- Files:
- Functions:
__ip_vs_dst_cache_reset()
ip_vs_dest_dst_rcu_free()
__ip_vs_cleanup_batch() / ip_vs_control_net_cleanup()
ip6_route_net_exit() / dst_entries_destroy()
dst_release() / dst_count_dec() / dst_entries_add()
percpu_counter_add_batch()
- Structs & Fields:
struct ip_vs_dest::dest_dst (struct ip_vs_dest_dst __rcu *)
struct ip_vs_dest_dst::dst_cache (struct dst_entry *)
struct dst_entry::ops (struct dst_ops *, pointing e.g. to net->ipv6.ip6_dst_ops)
struct dst_ops::pcpuc_entries (struct percpu_counter)
struct percpu_counter::counters (s32 __percpu *)
2. Triggering Flow and Lifecycle Invariant Violation
In IPVS, destinations (struct ip_vs_dest) can cache output route entries (struct dst_entry *) inside an RCU-protected wrapper struct struct ip_vs_dest_dst. When a destination route cache is invalidated or the destination is unlinked (such as during service teardown or device down events), __ip_vs_dst_cache_reset() in net/netfilter/ipvs/ip_vs_ctl.c unlinks the cache entry and defers releasing it:
static void __ip_vs_dst_cache_reset(struct ip_vs_dest *dest)
{
struct ip_vs_dest_dst *old;
old = rcu_dereference_protected(dest->dest_dst, 1);
if (old) {
RCU_INIT_POINTER(dest->dest_dst, NULL);
call_rcu(&old->rcu_head, ip_vs_dest_dst_rcu_free);
}
}
The RCU callback ip_vs_dest_dst_rcu_free() releases the held destination entry reference via dst_release(dest_dst->dst_cache):
void ip_vs_dest_dst_rcu_free(struct rcu_head *head)
{
struct ip_vs_dest_dst *dest_dst = container_of(head,
struct ip_vs_dest_dst,
rcu_head);
dst_release(dest_dst->dst_cache);
kfree(dest_dst);
}
During network namespace destruction (cleanup_net() in net/core/net_namespace.c), ops_undo_list() iterates through pernet_list in reverse order of subsystem registration, executing each subsystem's .exit or .exit_batch method:
-
IPVS Netns Cleanup:
IPVS is registered as a pernet_subsys (ipvs_core_ops) after the core routing protocols. Its exit handler __ip_vs_cleanup_batch() executes first, calling ip_vs_service_nets_cleanup() -> ip_vs_flush() -> __ip_vs_del_service() -> __ip_vs_unlink_dest() -> __ip_vs_dst_cache_reset(). This enqueues an RCU callback to ip_vs_dest_dst_rcu_free().
However, __ip_vs_cleanup_batch() does not call rcu_barrier() (unlike full module unload in ip_vs_cleanup(), which relies on rcu_barrier()).
-
Routing / DST Ops Teardown:
ops_undo_list() continues to later exit callbacks in the reverse list, eventually calling ip6_route_net_exit() (and/or xfrm*_net_exit()). In ip6_route_net_exit():
dst_entries_destroy(&net->ipv6.ip6_dst_ops);
dst_entries_destroy() invokes percpu_counter_destroy() -> percpu_counter_destroy_many(), which frees the per-CPU counter memory and explicitly zeroes the pointer:
free_percpu(fbc[0].counters);
for (i = 0; i < nr_counters; i++)
fbc[i].counters = NULL;
-
Deferred Callback Execution:
Subsequently, after an RCU grace period elapses, softirq context executes rcu_do_batch() and runs ip_vs_dest_dst_rcu_free().
ip_vs_dest_dst_rcu_free() calls dst_release(dest_dst->dst_cache).
In net/core/dst.c:
void dst_release(struct dst_entry *dst)
{
if (dst && rcuref_put(&dst->__rcuref)) {
...
dst_count_dec(dst);
call_rcu_hurry(&dst->rcu_head, dst_destroy_rcu);
}
}
dst_count_dec() calls dst_entries_add(dst->ops, -1).
In include/net/dst_ops.h:
static inline void dst_entries_add(struct dst_ops *dst, int val)
{
percpu_counter_add_batch(&dst->pcpuc_entries, val,
DST_PERCPU_COUNTER_BATCH);
}
3. Mechanics of the Page Fault Crash
In lib/percpu_counter.c:
void percpu_counter_add_batch(struct percpu_counter *fbc, s64 amount, s32 batch)
{
s64 count;
unsigned long flags;
count = this_cpu_read(*fbc->counters);
Because %r12 is 0x0, the instruction attempts to read offset 0x0 relative to %gs. Since the kernel per-CPU GS segment base (ffff8880d4775000) has offset 0 unmapped to catch NULL per-CPU dereferences, this supervisor read immediately causes a page fault:
BUG: unable to handle page fault for address: ffff8880d4775000
#PF: supervisor read access in kernel mode
RIP: 0010:percpu_counter_add_batch+0x4b/0x230 lib/percpu_counter.c:98
Because the fault occurs in softirq / IRQ context during RCU callback processing, it results in an unhandled kernel Oops.