Skip to content

fix: dlsym hook self-lookup degrades to RTLD_DEFAULT, crashing cuDNN 9 apps (silent SIGSEGV at first convolution)#215

Open
mohhef wants to merge 1 commit into
Project-HAMi:mainfrom
mohhef:fix-dlsym-self-handle-recursion
Open

fix: dlsym hook self-lookup degrades to RTLD_DEFAULT, crashing cuDNN 9 apps (silent SIGSEGV at first convolution)#215
mohhef wants to merge 1 commit into
Project-HAMi:mainfrom
mohhef:fix-dlsym-self-handle-recursion

Conversation

@mohhef

@mohhef mohhef commented Jul 6, 2026

Copy link
Copy Markdown

Symptom

Any LibTorch >= 2.5 cu12x application preloaded with libvgpu.so from a path other than /usr/local/vgpu/libvgpu.so dies with a silent SIGSEGV at its first convolution (no HAMI-core error banner), at every CUDA_DEVICE_MEMORY_LIMIT including very loose ones, and even with LD_PRELOAD set but no limit configured at all. On cuDNN 8 stacks (torch cu118) the same defect can surface as a livelock instead of a crash. This looks related to the reports in #169 (vllm 0.18 = torch 2.7 + cu128).

gdb backtrace at the crash (20 identical frames — infinite self-recursion until stack overflow):

Thread 1 received signal SIGSEGV, Segmentation fault.
0x00007.. in cudnnCreate () from /opt/libtorch/lib/libcudnn.so.9
#0  cudnnCreate () from libcudnn.so.9
#1  cudnnCreate () from libcudnn.so.9     <- same return address, recursing
#2  cudnnCreate () from libcudnn.so.9
... (identical frames all the way down)

Root cause

In src/libvgpu.c, the dlsym hook resolves every "cu"-prefixed symbol against a libvgpu self-handle:

vgpulib = dlopen("/usr/local/vgpu/libvgpu.so", RTLD_LAZY);   // fails on nonstandard installs
...
if (symbol[0] == 'c' && symbol[1] == 'u') {
    ...
    void *f = real_dlsym(vgpulib, symbol);   // vgpulib == NULL here

When libvgpu is preloaded from any other location (bind-mounted container paths, /opt/hami/..., etc.) and CUDA_REDIRECT is unset, vgpulib is NULL — and on glibc, dlsym(NULL, ...) means RTLD_DEFAULT: a global-scope search.

The "cu" prefix also matches CUDA library symbols, not just driver API. cuDNN 9 is a dispatcher architecture: libcudnn.so.9's cudnnCreate lazily resolves the real engine implementation via dlsym at first use. The interposed global-scope search hands the dispatcher its own cudnnCreate export back, and the self-call recurses until the stack overflows. Additionally, the unconditional pthread_once(preInit) for any cu* symbol runs full CUDA pre-initialization inside the caller's dynamic-loader call (the caller is mid-dlopen of cuDNN at that moment).

Fix (three small parts, one function)

  1. Recover the self-handle via dladdr on a known own symbol, dlopen(dli_fname, RTLD_LAZY | RTLD_NOLOAD), when the hardcoded path misses — the lookup is then always genuinely against libvgpu.
  2. Never RTLD_DEFAULT: if the self-handle is still NULL, treat the lookup as a miss and fall through to real_dlsym(handle, symbol) with the caller's own handle.
  3. preInit only on a confirmed hook hit, so non-driver cu* symbols (cudnnCreate, cublas*, cufft*, ...) no longer trigger CUDA pre-initialization inside the loader. (Hooks self-initialize via ensure_post_init, so deferral is safe.)

Deployments using the standard /usr/local/vgpu install path are unaffected by construction: the new code paths never execute there.

Verification

Case Before After
LibTorch 2.7.1+cu128 stereo-depth app (TorchScript), preloaded from /opt/hami/ SIGSEGV at first conv, every cap, even with no cap set Completes; caps enforced 12 GB down to 1 GB
Same app, LibTorch 2.4.1+cu118 build Livelock (100% CPU, 0% GPU, stuck in first inference) Completes
PyTorch cu118 application (previously working) works works, output numerically unchanged
C++ LibTorch cu118 application (previously working) works works, output numerically unchanged
nvidia-smi binding sanity, 2048 MiB cap 2048 MiB reported 2048 MiB reported

Possible follow-up (not in this PR)

The underlying discrimination mechanism ("prefix-match cu*, resolve against own exports") could instead gate on the explicit hook table (__dlsym_hook_section), which would also make the behavior independent of ELF export visibility. That requires reworking the table's pre-init early-return, so it is left as a follow-up.

Summary by CodeRabbit

  • Bug Fixes
    • Improved CUDA symbol lookup reliability when libvgpu is loaded from a different path than expected.
    • Prevented failed lookups from falling back to an invalid library handle, reducing load-time issues.
    • Made CUDA symbol resolution more stable by avoiding unnecessary initialization when a symbol is not available in libvgpu.

…9 apps

When libvgpu.so is preloaded from a path other than the hardcoded
/usr/local/vgpu/libvgpu.so (and CUDA_REDIRECT is unset), the dlopen for
the self-handle fails and vgpulib stays NULL. The dlsym hook then calls
real_dlsym(NULL, symbol) for every "cu"-prefixed symbol, which glibc
treats as RTLD_DEFAULT: a global-scope search.

That prefix also matches CUDA *library* symbols, not just driver API.
cuDNN 9 is a dispatcher architecture: libcudnn.so.9's cudnnCreate
resolves the real engine implementation via dlsym at first use. The
global-scope search hands the dispatcher its OWN cudnnCreate export
back, and the resulting self-call recurses until stack overflow --
a silent SIGSEGV with no HAMI-core diagnostics. Any LibTorch >= 2.5
cu12x application dies at its first convolution (TorchScript or eager);
with cuDNN 8 stacks the same mis-resolution can surface as a livelock
instead. Related report: Project-HAMi#169 (vllm 0.18 = torch 2.7 + cu128).

Fix, three parts:
1. Recover the self-handle via dladdr on a known own symbol and
   dlopen(dli_fname, RTLD_LAZY | RTLD_NOLOAD) when the hardcoded path
   misses, so the lookup is always against libvgpu itself.
2. If the self-handle is still NULL, treat the lookup as a miss and
   fall through to real_dlsym(handle, symbol) with the caller's own
   handle -- never RTLD_DEFAULT.
3. Fire preInit only after a confirmed hook hit, so non-driver
   "cu"-prefixed symbols (cudnnCreate, cublas*, ...) no longer trigger
   CUDA pre-initialization inside the caller's dynamic-loader call.

Verified: a LibTorch 2.7.1+cu128 stereo-depth application that
previously segfaulted at its first convolution under every memory cap
(including with LD_PRELOAD alone and no cap set) now runs to completion
with caps enforced from 12 GB down to 1 GB; a LibTorch 2.4.1+cu118
build of the same application that previously livelocked now completes.
No regression on previously-working workloads: PyTorch cu118 and C++
LibTorch cu118 pipelines both pass with unchanged accuracy under the
patched library. Deployments using the standard /usr/local/vgpu install
path are unaffected by construction (the new code path never executes).
@hami-robot hami-robot Bot requested a review from archlitchi July 6, 2026 15:29
@hami-robot

hami-robot Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for your pull request. Before we can look at it, you'll need to add a 'DCO signoff' to your commits.

📝 Please follow instructions in the contributing guide to update your commits with the DCO

Full details of the Developer Certificate of Origin can be found at developercertificate.org.

The list of commits missing DCO signoff:

  • 33c4664 fix: dlsym hook self-lookup degrades to RTLD_DEFAULT, crashing cuDNN 9 apps
Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@hami-robot

hami-robot Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: mohhef
Once this PR has been reviewed and has the lgtm label, please assign archlitchi for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@hami-robot hami-robot Bot requested a review from chaunceyjiang July 6, 2026 15:29
@hami-robot

hami-robot Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Welcome @mohhef! It looks like this is your first PR to Project-HAMi/HAMi-core 🎉

@hami-robot hami-robot Bot added the size/M label Jul 6, 2026
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The dlsym() implementation in libvgpu.c was updated to recover a valid libvgpu handle via dladdr()/dlopen(RTLD_NOLOAD) when the initial dlopen() fails, and to defer preInit invocation for cu*-prefixed symbols until after successful resolution against libvgpu, rather than triggering it unconditionally.

Changes

libvgpu dlsym Recovery and preInit Reordering

Layer / File(s) Summary
dlopen fallback recovery
src/libvgpu.c
When the initial dlopen() of vgpulib fails, recovers a valid handle by using dladdr() on init_dlsym to find the loaded libvgpu path, then reopens it with dlopen(..., RTLD_LAZY | RTLD_NOLOAD) instead of leaving vgpulib as NULL.
Deferred preInit for cu-symbols
src/libvgpu.c
For "cu"-prefixed symbol lookups, resolves the symbol against vgpulib first and only runs preInit via pthread_once (excluding "cuGetExportTable") when the symbol is found, falling through to real_dlsym(handle, symbol) otherwise.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant dlsym
    participant dladdr
    participant vgpulib
    participant preInit

    Caller->>dlsym: dlsym(handle, symbol)
    alt vgpulib is NULL
        dlsym->>dladdr: dladdr(init_dlsym)
        dladdr-->>dlsym: loaded library path
        dlsym->>vgpulib: dlopen(path, RTLD_NOLOAD)
    end
    alt symbol starts with "cu"
        dlsym->>vgpulib: real_dlsym(vgpulib, symbol)
        alt symbol found and not cuGetExportTable
            dlsym->>preInit: pthread_once(preInit)
        end
        dlsym-->>Caller: return resolved or fallback symbol
    else
        dlsym->>dlsym: real_dlsym(handle, symbol)
        dlsym-->>Caller: return symbol
    end
Loading

Poem

A hop, a skip, a dlopen fix,
When libraries scatter, I find their tricks. 🐇
preInit waits till the symbol's found,
No more reentrant loops going round.
Carrots and code, both safely stored!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the dlsym hook self-lookup bug and the cuDNN 9 crash it fixes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant