Crash due to UBSan in `ReleaseSafe`, but not `Debug`

I had an issue when building Neovim with Zig in ReleaseSafe mode.

Here is a minimal repro: Minimal reproduction for a UBSan error in ReleaseSafe but not Debug optimize mode · GitHub

You can clone it with this:

git clone https://gist.github.com/abcdc76fc9f97994939b87d550937f45.git ./ubsan-fail
cd ubsan-fail

UBSan causes the program to crash in ReleaseSafe mode, but not in Debug mode.

Why is that? Is UBSan only enabled for ReleaseSafe mode, and not Debug mode?

1 Like

I’m not 100% sure what is happening here, but I lowered your repro further:

#include <stddef.h>
#include <stdlib.h>

typedef struct {
  double x;
  char y;
  char fam[];
} FlexArray;

int main(void) {
  char *key = "howdy, partner!";
#define offset offsetof(FlexArray, fam)
  FlexArray *m = malloc(15);
  m->fam[sizeof(FlexArray) - offset - 2] = 0;
  return 0;
}
$ clang test.c -fsanitize=undefined
$ ./a.out
$ clang test.c -fsanitize=undefined -O2
$ ./a.out
test.c:13:6: runtime error: member access within address 0x557338097350 with insufficient space for an object of type 'FlexArray'
0x557338097350: note: pointer points here
 00 00 00 00  97 80 33 57 05 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  a1 0c 02 00
              ^
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior test.c:13:6
$ clang test.c -fsanitize=undefined -O2 -fno-builtin
$ ./a.out

So this does not seem to be Zig specific. Instead, clang knows the size of the malloc and now knows that the memory is too short. Instead, clang optimizes away the malloc to the stack and then triggers the ubsan.

1 Like

Odd, I thought it was only ReleaseFast that disabled UBSAN:

Your issue may be a bug in Zig.

ubsan is enabled by default on Debug and ReleaseSafe and disabled by default on ReleaseFast and ReleaseSmall (see e.g. #22488).

What I think is happening here, is that clang tries to detect this ub with the @llvm.objectsize intrinsic.

Since in Debug the input to this intrinsic is just a pointer, @llvm.objectsize reports an unknown size and ubsan does no trigger.

In ReleaseSafe the clang/llvm optimizer added the dereferenceable_or_null(15) attribute to the input pointer of @llvm.objectsize, since this is now known to be the return value of malloc(15). Because of this @llvm.objectsize can report 15 as the size and the ubsan can trigger. See this optimization step.

2 Likes

Do you think this would be considered a bug with Zig? If so, I can create a bug report with your simplified repro.

Edit: Oh, I didn’t fully comprehend your example :man_facepalming: this is also a problem in clang. So, is it fixable in Zig without changes to upstream?

I don’t think this is really fixable any way in both zig and clang. The problem is, that in unoptimized modes, it is not known how “long” the pointer is. Maybe it is possible in the address sanitizer to check if all bytes are accessible, but I don’t know. If you want to submit an issue, I would do it at the llvm project (If you do, please share the issue link here).