Data race on the per-`gdr_t` handle list in libgdrapi: `gdrcopy_sanity basic_child_thread_pins_buffer_*` crashes with SIGSEGV

オープン
#348 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
25/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
c, linux

調査の方向性

Start in src/gdrapi.c with gdr_pin_buffer, gdr_unpin_buffer and gdr_close, which update the per-handle g->memhs list without a lock. The failing test is basic_child_thread_pins_buffer_cumemalloc in tests/sanity.cpp, run under concurrent load. Done means the list updates are safe under concurrent pin and unpin, and the sanity loop no longer crashes. Deciding the locking approach needs maintainer input.

索引モデルが issue の本文から書いたものです。

説明

gdr_pin_buffer*() and gdr_unpin_buffer() update the per-handle list g->memhs with LIST_INSERT_HEAD / LIST_REMOVE and no lock.

When two threads pin and unpin on the same gdr_t concurrently (which gdrcopy_sanity basic_child_thread_pins_buffer_{cumemalloc,vmmalloc} does on purpose, and which the project has described as supported, see #152), the two list updates can interleave so that a node freed by gdr_unpin_buffer() stays reachable from the list head.

gdr_close() later walks the list and unpins the freed node: gdrdrv rejects the clobbered handle and LIST_REMOVE dereferences the clobbered le_next, so the test child dies with SIGSEGV and the harness prints Error: The test process got terminated.

We see this in our automated image validation at roughly 0.5 % of gdrcopy_sanity runs. I was unable to synthetically reproduce by re-running the test on a loop, however, so the attached example was made by 8 processes in parallel to more consistently hit the issue. But I want to note that we have hit this with a single process running.

Environment

  • gdrcopy 2.5.1 from the redist packages: libgdrapi_2.5.1-1_amd64.Ubuntu22_04.deb (libgdrapi.so.2.5 sha256 211d27990fbf24511601c9f896787d4c1e75e8cd9132127d6789fcb7ddcb9ff8) and gdrcopy-tests_2.5.1-1_amd64.Ubuntu22_04+cuda13.0.deb (gdrcopy_sanity sha256 b5ec066d95edf808d01195e9154b5b7d222798dbd872b757d2996f275c1af2b5)
  • gdrdrv 2.5 ("Persistent mapping will be used")
  • NVIDIA driver 580.178.04, open kernel module
  • 8x H200 (HGX), Linux 7.0.14 (vendor build, x86_64), Ubuntu 24.04 host, test run inside an Ubuntu 22.04 container
  • Also observed with driver 595 and on aarch64 (GH200/GB200); never observed so far on our 6.8 / 6.17 kernels, but see "Trigger" below.

Reproduction

A single process looping the two tests on an idle node did not fail in 5800 iterations. Running one loop per GPU concurrently (so the pin/unpin ioctls of several processes contend inside gdrdrv / the NVIDIA driver) fails more consistently:

# as root, 8-GPU system, debs above installed
for d in 0 1 2 3 4 5 6 7; do
  ( for i in $(seq 500); do
      out=$(gdrcopy_sanity -v -d $d -t basic_child_thread_pins_buffer_cumemalloc -t basic_child_thread_pins_buffer_vmmalloc 2>&1)
      case $out in *'&&&& FAILED'*) echo "gpu$d iteration $i FAILED"; printf '%s\n' "$out" > fail-gpu$d-$i.log;; esac
    done ) &
done; wait

First failure after ~350 runs in total (GPU 4, iteration 44, about 8 minutes). The failing run's verbose output:

&&&& RUNNING basic_child_thread_pins_buffer_cumemalloc
spawning single child thread
pinning
mapping
unmapping
unpinning
spawning two children threads, splitting setup and teardown
pinning
mapping
unmapping
unpinning
spawning two children threads, concurrently pinning and mapping the same buffer
pinning
pinning
mapping
unmapping
unpinning
mapping
unmapping
unpinning
spawning cleanup child thread
Error: The test process got terminated
&&&& FAILED basic_child_thread_pins_buffer_cumemalloc

Kernel log at the same second:

gdrdrv:__gdrdrv_unpin_buffer:unexpected handle b60ec000 while unmapping buffer
traps: gdrcopy_sanity[267793] general protection fault ip:7b632ec763e9 sp:7b611991bdf0 error:0 in libgdrapi.so.2.5[33e9,7b632ec74000+3000]

Offset 0x33e9 in the 2.5.1 Ubuntu 22.04 build is gdr_unpin_buffer+0x49, the first store of LIST_REMOVE. The same two lines, same offset, appear in every one of our CI occurrences.

Core dump

gdb on the core of the crashed test child (binaries byte-identical to the redist packages):

Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00007b632ec763e9 in gdr_unpin_buffer () from /lib/x86_64-linux-gnu/libgdrapi.so.2
#1  0x00007b632ec76714 in gdr_close () from /lib/x86_64-linux-gnu/libgdrapi.so.2
#2  0x0000582236ed0d3e in ?? ()            <- thr_fun_cleanup() in tests/sanity.cpp
#3  start_thread () at ./nptl/pthread_create.c:419
#4  clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:62

=> 0x7b632ec763e9 <gdr_unpin_buffer+73>:  mov    %rdx,0x10(%rax)
rax  0x5cc4e68f36b22230    (mh->entries.le_next: not a pointer, memory reused after free)
rdx  0x582254674a08        (mh->entries.le_prev: valid heap address)

So gdr_close() (the cleanup thread) is removing a node whose memory was already freed and reused by gdr_unpin_buffer() in one of the two thr_fun_combined threads; the node was still linked from g->memhs. The main thread is parked in pthread_join. We can share the core and the exact binaries privately.

Analysis

Both threads in the third phase of the test do gdr_pin_buffer -> gdr_map -> gdr_unmap -> gdr_unpin_buffer on the same gdr_t. With thread A in gdr_unpin_buffer() and thread B in gdr_pin_buffer():

B: LIST_INSERT_HEAD  reads head -> A          (B.next = A)
A: LIST_REMOVE       *A.prev = A.next         (head = NULL); free(A)
B: LIST_INSERT_HEAD  A.prev = &B.next; head = B   (writes into freed A)
=> list: head -> B -> A(freed)

gdr_close() walks the list, calls gdr_unpin_buffer(g, A): the ioctl is sent with A's clobbered handle (gdrdrv: "unexpected handle"), then LIST_REMOVE(A) stores through A's clobbered le_next and faults.

The alignment needed is that the two threads' pin and unpin ioctls return back to back, which happens readily when the ioctls contend inside the driver (hence the multi-process reproduction, and probably the occasional hit on freshly booted CI hosts). Nothing GPU- or kernel-specific is involved: src/gdrapi.c has no lock at all (also true on current master and in the 2.6 tree).

fail-gpu4-iter44.log

gdb-analysis.txt

主要言語
C
スター
1.4k
フォーク
198
PR マージ指標
30日以内にマージされた PR はありません

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

NVIDIA/gdrcopy のほかの issue

NVIDIA/gdrcopy の issue をすべて見る

似ている issue

C の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。