Data race on the per-`gdr_t` handle list in libgdrapi: `gdrcopy_sanity basic_child_thread_pins_buffer_*` crashes with SIGSEGV
まだ誰も着手していません。
評価
- 難易度
- 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 sha256211d27990fbf24511601c9f896787d4c1e75e8cd9132127d6789fcb7ddcb9ff8) andgdrcopy-tests_2.5.1-1_amd64.Ubuntu22_04+cuda13.0.deb(gdrcopy_sanity sha256b5ec066d95edf808d01195e9154b5b7d222798dbd872b757d2996f275c1af2b5) - 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).
- 主要言語
- C
- スター
- 1.4k
- フォーク
- 198
- PR マージ指標
- 30日以内にマージされた PR はありません
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
NVIDIA/gdrcopy のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
-
Proposal: honor `TMPDIR` in gdrcopy package build scripts対応中かも @efegokdemir が 53 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
-
難易度 5/5 1週間以上 初心者へのやさしさ 35/100
-
難易度 3/5 1〜2日 初心者へのやさしさ 58/100
-
GDRCopy build/installation fails on kernel v6.17 and above再び着手できるかも @pakmarkthub が 135 日前に担当しましたが、オープン中のプルリクエストはありません。 オープン
似ている issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
メンテナーはふだん 1 日以内に返信
-
SPIDevice selects an active-high device as soon as it is created対応中かも @mikeysklar が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 73/100
adafruit/circuitpython#11869 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 78/100
gnif/LookingGlass#1352 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
kovidgoyal/kitty#10862 ·
メンテナーはふだん 1 日以内に返信