Clang Address Sanitizer Issues

I'm using llvm-mingw (llvm-mingw-20240417-ucrt-x86_64) to compile my C11 program on Windows 10. (https://github.com/mstorsjo/llvm-mingw)

My compile command resembles below.

clang std=c11 *.c -Wall -g -gcodeview -fsanitize=address --for-linker --pdb=$(BUILD_DIR)/$(EXE_NAME).pdb

If I run the produced executable and pdb in RemedyBG, I'm bombarded with endless access violation breaks as soon as the process starts. Is the clang address sanitizer not supported? Or am I doing something wrong?

Debugging works as expected if I remove -fsanitize=address


Edited by ssoher on

RemedyBG has support for address santizer though testing has been primary with MSVC (e.g., cl /Z7 /fsanitize=address test.c).

I'll take a closer look at this when I get a chance. One thing you can try before then is to see if the clang-cl driver gives you different results: clang-cl /Z7 /fsanitize=address test.c, if that is feasible for your codebase.

In any case, thanks for letting me know.

Thank you for the reply, I will experiment with the cl driver when I have the time. Please let me know if I can help in any way for testing the clang address sanitizer compatibility when you are able to do some work on it.


Edited by ssoher on
Replying to x13pixels (#30070)

There are a few issues that are causing address santizer built with the mingw toolchain to fail to compute the base shadow space address and range. I tried with llvm-mingw-20240417-ucrt-x86_64.

  • The DLL is named libclang_rt.asan...dll rather than the expected clang_rt.asan...dll.
    • Workaround: look for libclang_rt.asan* in addition to clang_rt.asan* in RemedyBG; required to handle initialization before shadow memory is set up
  • The initialization halts in ntdll.dll!memset, called from the asan DLL, rather than the asan DLL itself. This requires unwinding one frame to detect this prior to the initialization of the base shadow memory address.
    • Workaround: unwind a frame before testing for libclang_rt.asan*
  • The shadow memory dynamic address global variable is not part of the executable as expected.
    • Workaround: import the variable and then set it to a known global name. Then, add support for asan_shadow_memory_dynamic_address in RemedyBG.

Used the following for testing:

__declspec(dllimport) extern void *__asan_shadow_memory_dynamic_address;
static void *asan_shadow_memory_dynamic_address;

int main(int argc, char **argv)
{
   asan_shadow_memory_dynamic_address = __asan_shadow_memory_dynamic_address;

   char test[4];
   test[argc] = 0;

   return 0;
}

Invoked with a.exe a b c d, ran and got the address sanitizer output without any of the earlier exceptions:

50084==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00e7504ffda5 at pc 0x7ff6335f152c bp 0x00e7504ffd00 sp 0x00e7504ffd48
WRITE of size 1 at 0x00e7504ffda5 thread T0
    #0 0x7ff6335f152b in main c:\remedybg\tests\llvm-mingw\test.c:9
    #1 0x7ff6335f1310 in WinMainCRTStartup (C:\remedybg\tests\llvm-mingw\a.exe+0x140001310)
    #2 0x7ff6335f1365 in mainCRTStartup (C:\remedybg\tests\llvm-mingw\a.exe+0x140001365)
    #3 0x7ffaabd07343  (C:\Windows\System32\KERNEL32.DLL+0x180017343)
    #4 0x7ffaabe426b0  (C:\Windows\SYSTEM32\ntdll.dll+0x1800526b0)

Address 0x00e7504ffda5 is located in stack of thread T0 at offset 37 in frame
    #0 0x7ff6335f13af in main c:\remedybg\tests\llvm-mingw\test.c:5

  This frame has 1 object(s):
    [32, 36) 'test' (line 8) <== Memory access at offset 37 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp, SEH and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow c:\remedybg\tests\llvm-mingw\test.c:9 in main
Shadow bytes around the buggy address:
  0x00e7504ffb00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x00e7504ffb80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x00e7504ffc00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x00e7504ffc80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x00e7504ffd00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x00e7504ffd80: f1 f1 f1 f1[04]f3 f3 f3 00 00 00 00 00 00 00 00
  0x00e7504ffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x00e7504ffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x00e7504fff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x00e7504fff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x00e750500000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb

I'm wondering if there is a more straightforward way to get this to work as expected without all the workarounds.


Edited by x13pixels on Reason: typos
Replying to ssoher (#30071)

Interesting, thanks for taking the time to inspect the process and coming up with workarounds. In this case please feel free to tell me that llvm-mingw will not be officially supported because the way it's set up is f*** up :) I can switch my toolchain to llvm and clang directly.

In all honesty, since I'm on Windows and currently developing for Windows as my main target platform, I should be using Visual Studio build tools directly but I have some possible misconceptions about how the msvc is lagging behind in newer C language standard compliance. And I'm a little bit lazy about learning the new compiler flags, I've never compiled from the command line with cl.

Thanks.


Replying to x13pixels (#30072)

Note that there are different problem here than just using "mingw".

When you use clang you need to specify target you'll be compiling to - mingw or msvc. They are incompatible in many ways, for example, how CRT runtime is handled & provided. Mingw one uses gcc based CRT. You can always switch clang to use different target with -target x86_64-pc-windows-msvc vs -target x86_64-pc-windows-gnu - regardless where did you get clang from (from msys2 or official installer, or you built one yourself).

When switching to different target you will need to provide relevant runtime headers/libraries/sdk. In gnu case mingw provides it, in msvc case msvc & windows sdk provides them. Just "switching toolchain to llvm and clang" directly means you will still need either mingw or msvc/winsdk installation - to provide runtime headers & libraries.

Address sanitizer is another complication on top of it. gcc has its own sanitizer runtime. Clang has its own sanitizer runtime. And msvc provides its own sanitizer runtime. I have no experience with gcc one, but clang and msvc are quite a bit different. In general I find msvc one more stable when developing on windows. They are based on same source code, but msvc has extra tweaks & fixes on top of it. Clang one often has small bugs or incomplete features.

When compiling with clang you can use msvc sanitizer runtime by just linking to proper .lib files & providing msvc development dlls on path.

Ah, even more reasons to switch to msvc and compile with cl instead of using msvc runtime through llvm for no apparent advantage then. I think I should go with VS Build Tools and Windows 10 SDK for the least amount of headaches.

Your post has been illuminating, thanks so much for your input.


Replying to mmozeiko (#30074)

"I think I should go with VS Build Tools and Windows 10 SDK for the least amount of headaches."

Maybe take a look at:

PortableBuildTools - Portable VS Build Tools installer

https://github.com/Data-Oriented-House/PortableBuildTools


Edited by hiddenfrog on

Thanks for sharing, I wasn't aware of this project. It doesn't seem to offer any advantage for my use case currently. Installing the build tools with Visual Studio Installer without the IDE is an okay experience now compared to what it was some years ago. And I may require some other components from the vs installer later for future contract work anyways. But regardless, very good to know that it exists 👍


Replying to hiddenfrog (#30077)

Switched everything to msvc cl and batch files (instead of the previous gcc or clang and makefile). Everything works just fine, including the address sanitizer.

I'm a little bit dismayed that cl with the std:clatest or c11 flags do not compile the following but I'll live. These code produce the compile error C2099: initializer is not a constant, I thought these were allowed since C99 and both clang and gcc compile them just fine.

struct test test_instance {
    .some_vector = (Vector3){ 0, 0, 0 }
};

below also, same error

int a = 5;
int b = a;

But these are not about RemedyBG of course. Thanks everyone!

That test_instance declaration is not valid C11 code. It is C++ GNU extension. GCC and clang with just C11 enabled and without extensions will not compile it.

You probably meant to write this:

struct test test_instance = { 
    .some_vector = (Vector3){ 0, 0, 0 }
};

or just this:

struct test test_instance = { 
    .some_vector = { 0, 0, 0 }
};

Both of these are valid C99 code.

As for second code example - are you putting that such declaration globally? That is also not valid C11 code. It's GNU extension. If you'll ask GCC or Clang compiler to use only C11 (-std=c11 -pedantic -Werror) then it will not compile and produce error. Put const int a = 5; instead and then it will compile. In C you cannot have non-constant initializers in global scope.

For your test_instance it is easily solvable by not using C99 compound literal. Just use regular initialization as I showed above. For int a use const.


Edited by Mārtiņš Možeiko on
Replying to ssoher (#30079)

Oh thank you, learning C with by-default enabled gcc extensions is not a good idea I guess but I didn't even know they were extensions.

struct test test_instance = { 
    .some_vector = (Vector3){ 0, 0, 0 }
};

above does not compile, initializer is not a constant

struct test test_instance = { 
    .some_vector = { 0, 0, 0 }
};

above compiles

// global variables, but also file scope with `static` produce the same error
const int a = 5;
int b = a;

above does not compile, initializer is not a constant

#define a 5
int b = a;

above compiles.

I better go over the basics instead of blaming msvc. Once again, thank you very much mmozeiko.


Edited by ssoher on
Replying to mmozeiko (#30080)

Ah, you're right - even const is not enough for it. It's still not a "constant" initializer from C point of view. So yeah, typically you do defines in C for such things.


Replying to ssoher (#30081)