Issue 97627
Summary TSAN macOS - iterating load commands in TSAN can result in a segfault
Labels new issue
Assignees
Reporter kubkon
    While working on enabling TSAN on macOS in Zig, I have hit a rare issue with current TSAN implementation. Namely, if a sanitized image dynamically links a dylib which has `headerpad` forced to 0, TSAN will crash when initializing itself.

Short repro:

```c
// a.c
int foo = 42;

void dummy() {
 foo--;
}
```

```c
// main.c
#include <stdio.h>
#include <pthread.h>

void dummy();

int Global;

void *Thread1(void *x) {
  Global++;
  return NULL;
}

void *Thread2(void *x) {
  Global--;
  return NULL;
}

int main() {
  dummy();
 pthread_t t[2];
  pthread_create(&t[0], NULL, Thread1, NULL);
 pthread_create(&t[1], NULL, Thread2, NULL);
  pthread_join(t[0], NULL);
  pthread_join(t[1], NULL);
}
```

Now build with system clang or LLVM clang, but make sure to use LLVM's linker as Apple's linker ignores `headerpad=0` for images.

```
$ cc -shared a.c -Wl,-headerpad,0 -Wl,-install_name,@rpath/liba.dylib -o liba.dylib -fuse-ld=/opt/llvm18-release/bin/ld64.lld
ld64.lld: warning: directory not found for option -L/usr/local/lib
$  cc main.c -fsanitize=thread -la -L. -Wl,-rpath,. -fuse-ld=/opt/llvm18-release/bin/ld64.lld
ld64.lld: warning: directory not found for option -L/usr/local/lib
$ ./a.out
fish: Job 1, './a.out' terminated by signal SIGSEGV (Address boundary error)
```

Running in the debugger reveals where the bug is hidden:

```
$ lldb a.out
(lldb) target create "a.out"
Current executable set to 'a.out' (arm64).
(lldb) settings set target.disable-aslr false
(lldb) r
Process 19672 launched: 'a.out' (arm64)
Process 19672 stopped
* thread #1, stop reason = EXC_BAD_ACCESS (code=1, address=0x34bcf7975)
    frame #0: 0x00000001050e7eec libclang_rt.tsan_osx_dynamic.dylib`__sanitizer::MemoryMappingLayout::Next(__sanitizer::MemoryMappedSegment*) + 348
libclang_rt.tsan_osx_dynamic.dylib`__sanitizer::MemoryMappingLayout::Next:
-> 0x1050e7eec <+348>: ldr    w8, [x23]
    0x1050e7ef0 <+352>: cmp    w8, #0xc
    0x1050e7ef4 <+356>: b.ne   0x1050e7ee0               ; <+336>
 0x1050e7ef8 <+360>: ldr    w8, [x23, #0x8]
Target 0: (a.out) stopped.
(lldb) bt
* thread #1, stop reason = EXC_BAD_ACCESS (code=1, address=0x34bcf7975)
  * frame #0: 0x00000001050e7eec libclang_rt.tsan_osx_dynamic.dylib`__sanitizer::MemoryMappingLayout::Next(__sanitizer::MemoryMappedSegment*) + 348
    frame #1: 0x0000000105153c10 libclang_rt.tsan_osx_dynamic.dylib`__tsan::CheckAndProtect() + 104
 frame #2: 0x000000010515358c libclang_rt.tsan_osx_dynamic.dylib`__tsan::InitializePlatform() + 28
 frame #3: 0x00000001051330ec libclang_rt.tsan_osx_dynamic.dylib`__tsan::Initialize(__tsan::ThreadState*) + 280
    frame #4: 0x0000000105102fbc libclang_rt.tsan_osx_dynamic.dylib`__tsan::ScopedInterceptor::ScopedInterceptor(__tsan::ThreadState*, char const*, unsigned long) + 176
    frame #5: 0x0000000105114ff0 libclang_rt.tsan_osx_dynamic.dylib`wrap_strlcpy + 72
    frame #6: 0x00000001887476e8 libsystem_c.dylib`__guard_setup + 132
    frame #7: 0x00000001887474a8 libsystem_c.dylib`_libc_initializer + 72
    frame #8: 0x00000001955115f4 libSystem.B.dylib`libSystem_initializer + 168
    frame #9: 0x000000018852505c dyld`invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const::$_0::operator()() const + 168
    frame #10: 0x0000000188563308 dyld`invocation function for block in dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const + 340
    frame #11: 0x000000018855699c dyld`invocation function for block in dyld3::MachOFile::forEachSection(void (dyld3::MachOFile::SectionInfo const&, bool, bool&) block_pointer) const + 496
    frame #12: 0x00000001885062fc dyld`dyld3::MachOFile::forEachLoadCommand(Diagnostics&, void (load_command const*, bool&) block_pointer) const + 300
    frame #13: 0x0000000188555930 dyld`dyld3::MachOFile::forEachSection(void (dyld3::MachOFile::SectionInfo const&, bool, bool&) block_pointer) const + 192
    frame #14: 0x0000000188562e1c dyld`dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const + 516
    frame #15: 0x0000000188521070 dyld`dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const + 524
    frame #16: 0x000000018852ad28 dyld`dyld4::PrebuiltLoader::runInitializers(dyld4::RuntimeState&) const + 44
    frame #17: 0x000000018854435c dyld`dyld4::APIs::runAllInitializersForMain() + 84
    frame #18: 0x000000018850af7c dyld`dyld4::prepare(dyld4::APIs&, dyld3::MachOAnalyzer const*) + 3156
    frame #19: 0x0000000188509edc dyld`start + 1844
(lldb) 
```

Namely, `NextCommand` function in `sanitizer_procmaps_mac.cpp` makes an incorrect assumption that there always is zeroed out padding between the end of load command table and beginning of the first loadable section. Incidentally, zeroed out padding of size at least 8 bytes can be interpreted as `LC_NULL` load command with size `0` which would terminate the iterator. Forcing the padding to `0` will make the iterator iterate past the end of the load command table.

Faulty code in question: https://github.com/llvm/llvm-project/blob/c02e8f762a410e55581866c43636efcd6504c1bd/compiler-rt/lib/sanitizer_common/sanitizer_procmaps_mac.cpp#L333-L335
_______________________________________________
llvm-bugs mailing list
llvm-bugs@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs

Reply via email to