Similar to pr96502, the test case below shows that the effect of attribute alloc_size on warnings is also lost after inlining. I open this as a separate bug (and expect to raise others) because I expect the "fixes" to be different in each case. Like pr96502, this also came up in the following discussion: https://gcc.gnu.org/pipermail/gcc-patches/2020-August/551526.html $ cat x.c && gcc -O2 -S -Wall -Wextra -fdump-tree-optimized=/dev/stdout x.c int* f (int); __attribute__ ((alloc_size (1), noinline)) int* f0 (int n) { return f (n); } void h0 (void) { int *p = f0 (3); __builtin_memset (p, 0, 3 * sizeof p); // warning (good) } __attribute__ ((alloc_size (1))) int* f1 (int n) { return f (n); } void h1 (void) { int *p = f1 (3); __builtin_memset (p, 0, 3 * sizeof p); // missing warning } x.c: In function ‘h0’: x.c:9:3: warning: ‘__builtin_memset’ forming offset [3, 23] is out of the bounds [0, 3] [-Warray-bounds] 9 | __builtin_memset (p, 0, 3 * sizeof p); // warning (good) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Created attachment 53643 [details] PoC showing unexpected __bdos results across inlines Fixing this is needed for the Linux kernel to do much useful with alloc_size. Most of the allocators are inline wrappers, for example. This can be additionally shown to break __builtin_dynamic_object_size(), which means all FORTIFY_SOURCE of alloc_size-marked inlines is broken. :( https://godbolt.org/z/jTKjY3s1j
(In reply to Kees Cook from comment #1) > Created attachment 53643 [details] > PoC showing unexpected __bdos results across inlines > > Fixing this is needed for the Linux kernel to do much useful with > alloc_size. Most of the allocators are inline wrappers, for example. For cases where the size doesn't really change across the inlines, it ought to be sufficient to annotate the non-inlined implementation function, e.g. in case of kvmalloc, annotate kvmalloc_node as __alloc_size(1). For other cases it may be less trivial, e.g.: /* Some padding the wrapper adds to the actual allocation. */ size_t metadata_size; __attribute__ ((alloc_size (1))) void *alloc_wrapper (size_t sz) { return real_alloc (size + metadata_size); } extern void *real_alloc (size_t) __attribute__ ((alloc_size(1))); here the compiler will end up seeing the padded size, which may not be correct. To fix this we'll have to store the alloc_size info somewhere (ptr_info seems to be aliasing-specific, so maybe a new member to tree_ssa_name) during inlining and then teach the tree-object-size pass to access it.
Was just to file this bug... Note that the access attribute could be translated into the same builtin suggested for counted_by in https://gcc.gnu.org/pipermail/gcc-patches/2023-October/634177.html and then this could work. This would also simply the BDOS path I think because special code for the access attribute could go. Example: https://godbolt.org/z/1TTePn7hn static #if 0 char *propagate(char *p, int s) [[gnu::access(read_only, 1, 2)]] #else char *propagate(char *p; int s; char p[(p = pointer_with_size(p, s), s)], int s) #endif { printf("%ld\n", __builtin_dynamic_object_size(p, 0)); return p; }
This came up in the wild at https://github.com/systemd/systemd/issues/22801 and https://github.com/systemd/systemd/pull/25688.
This could work for alloc_size, but not quite for access. pointer_with_size (or __builtin_with_size as you suggested in that thread) would need to express access semantics too, to be able to express everything that access does.
So basically, __builtin_with_access(void *ptr, size_t size, int access) where access == -1: Unknown access semantics 0: none 1: read_only 2: write_only 3: read_write should address both access and alloc_size and even counted_by. We would need to emit the builtin in the caller as well as callee of the function that has the access attribute while for alloc_size, we only need to emit this in the caller.
Am Mittwoch, dem 25.10.2023 um 11:08 +0000 schrieb siddhesh at gcc dot gnu.org: > https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96503 > > --- Comment #6 from Siddhesh Poyarekar <siddhesh at gcc dot gnu.org> --- > So basically, > > __builtin_with_access(void *ptr, size_t size, int access) > > where access == > > -1: Unknown access semantics > 0: none > 1: read_only > 2: write_only > 3: read_write > > should address both access and alloc_size and even counted_by. Yes, sounds good. > We would need > to emit the builtin in the caller as well as callee of the function that has > the access attribute while for alloc_size, we only need to emit this in the > caller. Yes, makes sense, although I guess caller part for "access" is only for warning and not relevant for BDOS, so could potentially stay as it is for now. For __builtin_with_access we probably only want to allow reducing the object size, while the 'extend_size' workaround used by systemd (cf comment #4) would need to extend it. Maybe we need another flag? Martin
(In reply to Martin Uecker from comment #7) > For __builtin_with_access we probably only want to allow > reducing the object size, while the 'extend_size' workaround > used by systemd (cf comment #4) would need to extend it. > Maybe we need another flag? I've been thinking of a new __object_size__ attribute to annotate functions that behave like __builtin_object_size so that calls to it override (and not just reduce) sizes returned by allocations. I can then use it to annotate a supported malloc_usable_size replacement (say, malloc_size_estimate) that actually works like __builtin_object_size and can then be used by systemd.
This bug is particularly nasty when you have `alloc` and `resize` and the `alloc` call retains the `alloc_size` information but the `resize` call gets inlined (thus losing the new `alloc_size`) and now you're left with a pointer that GCC thinks has the *old* size. Here's a minimal demo: static int *arena; __attribute(( malloc, alloc_size(1), noinline )) static void *alloc(int size) { return arena++; } //__attribute((noinline)) __attribute(( alloc_size(2) )) static void *extend(void *oldptr, int newsize) { ++arena; return oldptr; } #include <stdlib.h> int main(void) { arena = malloc(4 * sizeof(int)); if (!arena) abort(); int *a = alloc(sizeof *a); a[0] = 4; a = extend(a, sizeof *a * 2); a[1] = 8; return a[1]; } (The `alloc` and `resize` function in practice is more sophisticated, but the above suffices for demo purposes). Compile with `gcc -O2 -fsanitize=address,undefined` and the `a[1]` will trigger UBSan because it's still operating on the old size information. Uncommenting the `noinline` from extend() "fixes" the issue. But littering the allocation routines with `noinline` is not really a good idea since it'd regress performance for custom allocators that have trivial logic (e.g bump allocators). This bug unfortunately makes the `alloc_size` attribute unusable for my purposes.