$ 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.
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.
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).