Sanitizer interception of deprecated symbols is incompatible with dlopen (plugins)
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
_(I am focusing on ASAN in the description but, I believe, all sanitizers are affected the same way.)_
At the core is a fundamental limitation of how interception interacts with loading objects via dlopen (e.g. plugins).
Here is a simplified reproducer:
```c
// plugin.c:
#include
#include
void foo() {
XDR xdrs;
xdrstdio_create(&xdrs, stdin, XDR_ENCODE);
xdr_destroy(&xdrs);
printf("OK\n");
}
```
```c
// main.c:
#include
#include
typedef void*(*foo_t)();
int main() {
void *handle = dlopen("./plugin.so", RTLD_LAZY);
assert(handle);
void *foo = dlsym(handle, "foo");
assert(foo);
((foo_t)foo)();
return 0;
}
```
(This requires the `libtirpc` package, which is where the deprecated symbol has been moved to.)
```shell_session
$ clang -ltirpc -fpic -shared plugin.c -o plugin.so
$ clang main.c -o main
$ ./main
OK
$ clang -fsanitize=address main.c -o main
$ ./main
AddressSanitizer:DEADLYSIGNAL
=================================================================
==2735207==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x000000000000 bp 0x7fffffffb570 sp 0x7fffffffad38 T0)
==2735207==Hint: pc points to the zero page.
==2735207==The signal is caused by a READ memory access.
==2735207==Hint: address points to the zero page.
#0 0x000000000000 ()
#1 0x7ffff78b013c (plugin.so+0x113c)
#2 0x5555556bbd46 (/tmp/main+0x167d46)
#3 0x7ffff7cc127d (/nix/store/-glibc-2.40-36/lib/libc.so.6+0x2a27d)
==2735207==Register values:
rax = 0x0000000000000001 rbx = 0x00007fffffffb580 rcx = 0x0000000000000000 rdx = 0x0000000000000000
rdi = 0x00007fffffffb580 rsi = 0x00007ffff7e818e0 rbp = 0x00007fffffffb570 rsp = 0x00007fffffffad38
r8 = 0x0000000000000000 r9 = 0x0000000000000000 r10 = 0x0000000000000000 r11 = 0x000055555559cd31
r12 = 0x0000000000000000 r13 = 0x00007fffffffb708 r14 = 0x00007ffff7ffd000 r15 = 0x00005555556f8790
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV ()
==2735207==ABORTING
```
Here is why this is happening:
1. On startup, `libclang_rt.asan` resolves the corresponding real functions for its interceptors.
2. The `xdrstdio_create` function has been deprecated in glibc, so `dlsym` returns nullptr.
3. `plugin.so` is loaded using `dlopen`; among its dependencies is `libtirpc.so`, which provides the symbol now.
4. The function in the plugin tries to call `xdrstdio_create`, the call is intercepted, and `___interceptor_xdrstdio_create` crashes calling nullptr.
_(Note: This only crashes with glibc >= 2.36. In earlier versions, `dlsym` in step 2 was returning the deprecated symbol, which is, arguably, even worse than a crash: #133338)._
This is Clang 19.1.6, but, judging by the code, I believe this should be reproducible in `main` as well.
One possible short-term solution would be to resolve the exact versions of the deprecated functions.
This would work as expected for legacy binaries, however new binaries (e.g. libnss_nis/libnsl) will end up with their calls being dispatched to unexpected versions of the symbols under sanitizers.
Probably a better quick solution is to remove the interceptors and accept potential false positive as a result.
Looking at the whole picture, this appears to be a fundamental limitation of how interceptors currently work.
I see two possible radical fixes:
1. Intercept `dlopen` and re-scan the symbols for any new real symbols to dispatch to (tricky).
2. Instead of eagerly resolving all real call targets on startup, postpone the `dlsym` invocation until the first actual call for each symbol (slower?).
Or, maybe, as a middle ground, keep resolving symbols at startup, but re-resolve if it is nullptr at the time it is about to be called.
Contributor guide
Research direction
Reproduce the failure with plugin.c and main.c, using the dlopen entry point foo and the ASAN interceptor for xdrstdio_create. Then trace how sanitizer startup resolves the real symbol and how the plugin loads libtirpc.so. Done means the reproducer no longer crashes under ASAN, with regression coverage for the dlopen case.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- devtools, operating-systems
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100