Linux kernel crash report

Oops reported via lkml (MSGID: 69ea344f.a00a0220.17a17.0040.GAE@google.com). Crash type: General Protection Fault / KASAN null-ptr-deref

Root cause: Double-release race on the RXE UDP tunnel socket — rxe_ns_exit() and rxe_net_del() can both call udp_tunnel_sock_release() on the same socket; the second call crashes in kernel_sock_shutdown() because sock->ops was set to NULL by the first sock_release(). Introduced by commits 13f2a53c2a71e and f1327abd6abed (March 2026 RXE per-namespace support).

Key elements

Field Value Implication
MSGID 69ea344f.a00a0220.17a17.0040.GAE@google.com
MSGID_URL 69ea344f.a00a0220.17a17.0040.GAE@google.com
UNAME syzkaller #0 PREEMPT(full) Syzkaller automated test kernel
DISTRO (none detected — syzkaller custom build)
PROCESS syz.7.1709 Syzkaller fuzzer process
HARDWARE QEMU Standard PC (Q35 + ICH9, 2009)
BIOS 1.16.3-debian-1.16.3-2 04/01/2014
TAINT L (softlockup) Indicates a system stall, thermal issues, or malicious software.
CRASH_TYPE General Protection Fault + KASAN null-ptr-deref
KASAN_RANGE [0x0000000000000068–0x000000000000006f] Null-pointer dereference at offset 0x68 (proto_ops→shutdown)
RIP kernel_sock_shutdown+0x47/0x70 net/socket.c:3785 Crash in kernel socket shutdown
HEAD_COMMIT 6596a02b2078 “Merge tag ‘drm-next-2026-04-22’”
SOURCEDIR oops-workdir/linux Checked out to commit 6596a02b
VMLINUX oops-workdir/syzbot/vmlinux-6596a02b Downloaded from syzbot assets

Kernel modules

Module Flags Backtrace Location Flag Implication
(no modules listed — built-in only)

Backtrace

Address Function Offset Size Context Module Source location
0xffffffff895ced47 (0xffffffff895ced00 + 0x47) kernel_sock_shutdown 0x47 0x70 Task (built-in) net/socket.c:3785
0xffffffff8a20ec48 (0xffffffff8a20ebe0 + 0x68) udp_tunnel_sock_release 0x68 0x80 Task (built-in) net/ipv4/udp_tunnel_core.c:202
rxe_release_udp_tunnel (inlined) Task (built-in) drivers/infiniband/sw/rxe/rxe_net.c:294
0xffffffff88c2b9ee (0xffffffff88c2b940 + 0xae) rxe_sock_put 0xae 0x130 Task (built-in) drivers/infiniband/sw/rxe/rxe_net.c:639
0xffffffff88c2fcd3 (0xffffffff88c2fc50 + 0x83) rxe_net_del 0x83 0x120 Task (built-in) drivers/infiniband/sw/rxe/rxe_net.c:660
0xffffffff88bef275 (0xffffffff88bef260 + 0x15) rxe_dellink 0x15 0x20 Task (built-in) drivers/infiniband/sw/rxe/rxe.c:254
0xffffffff88a0a2a9 (0xffffffff88a0a020 + 0x289) nldev_dellink 0x289 0x3c0 Task (built-in) drivers/infiniband/core/nldev.c:1849
0xffffffff889beda2 (0xffffffff889bea10 + 0x392) rdma_nl_rcv_msg 0x392 0x6f0 Task (built-in) drivers/infiniband/core/netlink.c:195
0xffffffff889bf3db (0xffffffff889bf110 + 0x2cb) rdma_nl_rcv_skb.constprop.0.isra.0 0x2cb 0x410 Task (built-in) drivers/infiniband/core/netlink.c:240
netlink_unicast_kernel (inlined) Task (built-in) net/netlink/af_netlink.c:1318
0xffffffff89aaa135 (0xffffffff89aa9bb0 + 0x585) netlink_unicast 0x585 0x850 Task (built-in) net/netlink/af_netlink.c:1344
0xffffffff89aaacc0 (0xffffffff89aaa410 + 0x8b0) netlink_sendmsg 0x8b0 0xda0 Task (built-in) net/netlink/af_netlink.c:1894
sock_sendmsg_nosec (inlined) Task (built-in) net/socket.c:787
__sock_sendmsg (inlined) Task (built-in) net/socket.c:802
0xffffffff895d4ce1 (0xffffffff895d4300 + 0x9e1) ____sys_sendmsg 0x9e1 0xb70 Task (built-in) net/socket.c:2698
0xffffffff895d8aa0 (0xffffffff895d8910 + 0x190) ___sys_sendmsg 0x190 0x1e0 Task (built-in) net/socket.c:2752
0xffffffff895dfdf0 (0xffffffff895dfc80 + 0x170) __sys_sendmsg 0x170 0x220 Task (built-in) net/socket.c:2784
do_syscall_x64 (inlined) Task (built-in) arch/x86/entry/syscall_64.c:63
0xffffffff8b9a318b (0xffffffff8b9a3080 + 0x10b) do_syscall_64 0x10b 0xf80 Task (built-in) arch/x86/entry/syscall_64.c:94
0xffffffff81000130 (0xffffffff810000b9 + 0x77) entry_SYSCALL_64_after_hwframe 0x77 Task (built-in) arch/x86/entry/entry_64.S:121

CPU Registers

Register Value Notes
RIP 0xffffffff895ced47 kernel_sock_shutdown+0x47 — crash site
RSP 0xffffc9000566f180 Kernel stack
EFLAGS 0x00010202
RAX 0xdffffc0000000000 KASAN shadow base address
RBX 0xffff888058587240 struct socket *sock (the socket being shut down)
RCX 0x0000000000000000
RDX 0x000000000000000d KASAN shadow offset = 0x68 >> 3 = 0xd
RSI 0xffffffff895ced12 Return address / callee
RDI 0x0000000000000068 Computed as sock->ops + 0x68 (shutdown fn ptr address); sock->ops = NULL
RBP 0x0000000000000002 how = SHUT_RDWR = 2
R08 0x0000000000000001
R09 0xffffed1006d98945
R10 0xffff888036cc4a2b
R11 0x0000003683c25c00
R12 0x0000000000000000 sock->ops loaded from RBX+0x20 = NULL — the null pointer
R13 0xffff88805c998000
R14 0x0000000000000002
R15 0x0000000000000018
CR0 0x0000000080050033
CR2 0x00007f1306d97d58 User-space address (unrelated)
CR3 0x00000000404f1000 Page table base
CR4 0x0000000000352ef0

Backtrace source code

1. kernel_sock_shutdown — crash site (net/socket.c:3785)

net/socket.c @ 6596a02b

3783  int kernel_sock_shutdown(struct socket *sock, enum sock_shutdown_cmd how)
3784  {
3785    return READ_ONCE(sock->ops)->shutdown(sock, how);   // ← CRASH: sock->ops is NULL
3786  }

At offset +0x47 (address 0xffffffff895ced47), the KASAN instrumentation checks the shadow byte for address 0x68 (= NULL + 0x68, the shutdown function pointer in struct proto_ops) and traps because this is a null-pointer dereference. R12 = sock->ops = 0x0 (loaded from RBX+0x20 where RBX = struct socket *sock).

Disassembly window around crash (from vmlinux):

ffffffff895ced37:mov    0x20(%rbx),%r12     ; r12 = sock->ops  (= NULL)
ffffffff895ced3b:lea    0x68(%r12),%rdi     ; rdi = &proto_ops->shutdown  (= 0x68)
ffffffff895ced40:mov    %rdi,%rdx
ffffffff895ced43:shr    $0x3,%rdx           ; KASAN shadow offset
ffffffff895ced47:cmpb   $0x0,(%rdx,%rax,1)  ; <<< CRASH: KASAN null-ptr check
ffffffff895ced4b:jne    ffffffff895ced67
ffffffff895ced4d:mov    0x68(%r12),%rax     ; would load proto_ops->shutdown
ffffffff895ced52:mov    %ebp,%esi
ffffffff895ced54:mov    %rbx,%rdi

2. udp_tunnel_sock_release (net/ipv4/udp_tunnel_core.c:204)

net/ipv4/udp_tunnel_core.c @ 6596a02b

198  void udp_tunnel_sock_release(struct socket *sock)
199  {
200     rcu_assign_sk_user_data(sock->sk, NULL);
201     synchronize_rcu();
202     kernel_sock_shutdown(sock, SHUT_RDWR);   // ← calls crash site; sock->ops is NULL here
203     sock_release(sock);
204  }

Backtrace entry at offset +0x68/0x80, source line 204 (annotated by syzbot).

3. rxe_release_udp_tunnel (inlined) (drivers/infiniband/sw/rxe/rxe_net.c:295)

drivers/infiniband/sw/rxe/rxe_net.c @ 6596a02b

291  static void rxe_release_udp_tunnel(struct socket *sk)
292  {
293     if (sk)
294         udp_tunnel_sock_release(sk);
295  }

Inlined into rxe_sock_put. The call at line 294 passes sk->sk_socket (see rxe_sock_put below). The guard checks sk != NULL but does not check sk->ops.

4. rxe_sock_put (drivers/infiniband/sw/rxe/rxe_net.c:639)

drivers/infiniband/sw/rxe/rxe_net.c @ 6596a02b

632  static void rxe_sock_put(struct sock *sk,
633                     void (*set_sk)(struct net *, struct sock *),
634                     struct net *net)
635  {
636     if (refcount_read(&sk->sk_refcnt) > SK_REF_FOR_TUNNEL) {
637         __sock_put(sk);
638     } else {
639         rxe_release_udp_tunnel(sk->sk_socket);   // ← calls with sk->sk_socket
640         sk = NULL;
641         set_sk(net, sk);
642     }
643  }

SK_REF_FOR_TUNNEL = 2 (defined at line 23 of rxe_net.c). When sk->sk_refcnt <= 2, the else branch is taken and rxe_release_udp_tunnel(sk->sk_socket) is called. The struct socket * passed is sk->sk_socket.

Factual observation: rxe_sock_put calls rxe_release_udp_tunnel(sk->sk_socket) without checking whether sk->sk_socket->ops is non-NULL.


Phase 2 Analysis

What

kernel_sock_shutdown() dereferences sock->ops (a const struct proto_ops *) which is NULL at the time of the call.

net/socket.c @ 6596a02b

3783  int kernel_sock_shutdown(struct socket *sock, enum sock_shutdown_cmd how)
3784  {
3785    return READ_ONCE(sock->ops)->shutdown(sock, how);   // ← CRASH: sock->ops is NULL
3786  }

KASAN confirms a null-pointer dereference at offset 0x68 — which matches offsetof(struct proto_ops, shutdown) (confirmed from the struct layout with CONFIG_COMPAT=y). Register R12 = 0x0 (sock->ops), loaded from RBX+0x20 (struct socket.ops at offset 0x20), proves ops == NULL.

The crash is called from udp_tunnel_sock_release() (net/ipv4/udp_tunnel_core.c) which is called from rxe_release_udp_tunnel() (inlined) inside rxe_sock_put(), as part of the rxe_net_del() RDMA link-delete path triggered via nldev_dellinkrxe_dellinkrxe_net_del.

How

Q1: How did sock->ops become NULL at the point of the crash?

A1: sock->ops is set to NULL inside sock_release() at net/socket.c:726:

718  ops->release(sock);
719  sock->sk = NULL;
...
726  sock->ops = NULL;

The call to udp_tunnel_sock_release() that crashes attempts:

kernel_sock_shutdown(sock, SHUT_RDWR);   // ← crash — sock->ops already NULL
sock_release(sock);

kernel_sock_shutdown() is called before sock_release() within udp_tunnel_sock_release(). Therefore sock->ops cannot be NULL on the first call — it must have been set to NULL by a prior sock_release() on the same socket.

Q2: Who called sock_release() on this socket before rxe_net_del() ran?

A2: A second, independent code path — rxe_ns_exit() in drivers/infiniband/sw/rxe/rxe_ns.c — can release the same socket when the network namespace is destroyed:

static void rxe_ns_exit(struct net *net)
{
    struct rxe_ns_sock *ns_sk = net_generic(net, rxe_pernet_id);
    struct sock *sk;

    rcu_read_lock();
    sk = rcu_dereference(ns_sk->rxe_sk4);     // read pointer
    rcu_read_unlock();
    if (sk) {
        rcu_assign_pointer(ns_sk->rxe_sk4, NULL);      // clear pointer
        udp_tunnel_sock_release(sk->sk_socket);         // release socket → sock->ops = NULL
    }
    ...
}

And rxe_net_del() in rxe_net.c:

void rxe_net_del(struct ib_device *dev)
{
    ...
    sk = rxe_ns_pernet_sk4(net);   // read pointer (no atomic ownership transfer)
    if (sk)
        rxe_sock_put(sk, rxe_ns_pernet_set_sk4, net);
    ...
}

rxe_sock_put() releases without first clearing:

static void rxe_sock_put(struct sock *sk, ...)
{
    if (refcount_read(&sk->sk_refcnt) > SK_REF_FOR_TUNNEL) {
        __sock_put(sk);
    } else {
        rxe_release_udp_tunnel(sk->sk_socket);  // release FIRST (bug)
        sk = NULL;
        set_sk(net, sk);                         // clear AFTER (too late)
    }
}

Q3: How does the race materialise?

A3: The following TOCTOU sequence is possible when both namespace teardown and RDMA link deletion happen concurrently (as syzkaller exercises):

Thread A (rxe_net_del):        Thread B (rxe_ns_exit):
  rxe_ns_pernet_sk4() → sk=X
                                 rcu_assign_pointer(sk4, NULL)
                                 udp_tunnel_sock_release(X->sk_socket)
                                   sock_release(X->sk_socket)
                                     X->sk_socket->ops = NULL   ← clears ops
  rxe_sock_put(sk=X, ...)
    rxe_release_udp_tunnel(X->sk_socket)
      udp_tunnel_sock_release(X->sk_socket)
        kernel_sock_shutdown(X->sk_socket, SHUT_RDWR)
          READ_ONCE(sock->ops)->shutdown(...)   ← CRASH: ops == NULL

rxe_ns_exit() atomically clears the pernet pointer before releasing, but rxe_net_del() reads the pointer independently (without taking ownership atomically). By the time rxe_net_del() acts on the pointer, rxe_ns_exit() may have already released the socket.

Note: even without a race, rxe_ns_exit() is also triggered by the NETDEV_UNREGISTER notifier at rxe_net.c:725 which also calls rxe_net_del(), creating a double-invocation on a single-namespace teardown if both fire.

Where

Bug introduction commits (git count: 2):

Hash Subject Role
13f2a53c2a71e RDMA/rxe: Add net namespace support for IPv4/IPv6 sockets Added rxe_ns.c with rxe_ns_exit()
f1327abd6abed RDMA/rxe: Support RDMA link creation and destruction per net namespace Added rxe_net_del() with rxe_sock_put()

Both commits authored by Zhu Yanjun <yanjun.zhu@linux.dev>, merged 2026-03-12. Neither commit synchronises ownership of the socket between the two teardown paths.

Recommended fix:

Replace rxe_ns_pernet_sk4() in rxe_net_del() (and rxe_notify()) with an atomic exchange that simultaneously reads and clears the pointer, so only one of the two teardown paths can ever obtain a non-NULL socket:

--- a/drivers/infiniband/sw/rxe/rxe_ns.h
+++ b/drivers/infiniband/sw/rxe/rxe_ns.h
 struct sock *rxe_ns_pernet_sk4(struct net *net);
 void rxe_ns_pernet_set_sk4(struct net *net, struct sock *sk);
+struct sock *rxe_ns_pernet_take_sk4(struct net *net);
 struct sock *rxe_ns_pernet_sk6(struct net *net);
 void rxe_ns_pernet_set_sk6(struct net *net, struct sock *sk);
+struct sock *rxe_ns_pernet_take_sk6(struct net *net);
--- a/drivers/infiniband/sw/rxe/rxe_ns.c
+++ b/drivers/infiniband/sw/rxe/rxe_ns.c
+struct sock *rxe_ns_pernet_take_sk4(struct net *net)
+{
+struct rxe_ns_sock *ns_sk = net_generic(net, rxe_pernet_id);
+
+return unrcu_pointer(xchg(&ns_sk->rxe_sk4,
+   RCU_INITIALIZER(NULL)));
+}
+
+struct sock *rxe_ns_pernet_take_sk6(struct net *net)
+{
+struct rxe_ns_sock *ns_sk = net_generic(net, rxe_pernet_id);
+
+return unrcu_pointer(xchg(&ns_sk->rxe_sk6,
+   RCU_INITIALIZER(NULL)));
+}
--- a/drivers/infiniband/sw/rxe/rxe_net.c
+++ b/drivers/infiniband/sw/rxe/rxe_net.c
 void rxe_net_del(struct ib_device *dev)
 {
 ...
-sk = rxe_ns_pernet_sk4(net);
+sk = rxe_ns_pernet_take_sk4(net);    /* atomically claim ownership */
 if (sk)
-rxe_sock_put(sk, rxe_ns_pernet_set_sk4, net);
+udp_tunnel_sock_release(sk->sk_socket);  /* sk4 already cleared */

-sk = rxe_ns_pernet_sk6(net);
+sk = rxe_ns_pernet_take_sk6(net);
 if (sk)
-rxe_sock_put(sk, rxe_ns_pernet_set_sk6, net);
+udp_tunnel_sock_release(sk->sk_socket);
 ...
 }

The xchg ensures that whichever of rxe_ns_exit() or rxe_net_del() runs first atomically obtains the socket pointer; the second caller gets NULL and skips the release. This eliminates the TOCTOU window.

An alternative minimal fix (if the xchg approach is considered too intrusive) is to check sock->ops != NULL inside udp_tunnel_sock_release() before calling kernel_sock_shutdown(), but this treats the symptom rather than the race.

Patch

Key refinements vs. rough patch: - Added proper indentation in rxe_ns.c function bodies - Used xchg((__force struct sock **)&ns_sk->rxe_sk4, NULL) instead of unrcu_pointer(xchg(&ns_sk->rxe_sk4, RCU_INITIALIZER(NULL))) — avoids type ambiguity with a NULL argument to RCU_INITIALIZER() - Added missing static inline rxe_ns_pernet_take_sk6() stub in the #else /* IPv6 */ branch of rxe_ns.h (required for non-IPv6 builds) - Filled real context lines in rxe_net.c hunk (replaced sketch ...)

Fact Check

All checked items verified. Minor line number offsets (±1 to ±2 lines) found in udp_tunnel_sock_release and rxe_release_udp_tunnel citations; these are cosmetic drift and do not affect correctness of the identified functions or analysis. See factcheck.md for details.