[clang] [C] Warn on uninitialized const objects (PR #137166)
Aaron Ballman via cfe-commits
cfe-commits at lists.llvm.org
Tue Apr 29 08:06:32 PDT 2025
AaronBallman wrote:
> > Whhhhaaaa? `__unqual_scalar_typeof(*p)` seems to result in a `const`-qualified type. I'm surprised something named `unqual` would do that. Can you confirm I'm getting that correct?
>
> It looks like this is
>
> ```c
> /*
> * __unqual_scalar_typeof(x) - Declare an unqualified scalar type, leaving
> * non-scalar types unchanged.
> */
> /*
> * Prefer C11 _Generic for better compile-times and simpler code. Note: 'char'
> * is not type-compatible with 'signed char', and we define a separate case.
> */
> #define __scalar_type_to_expr_cases(type) \
> unsigned type: (unsigned type)0, \
> signed type: (signed type)0
>
> #define __unqual_scalar_typeof(x) typeof( \
> _Generic((x), \
> char: (char)0, \
> __scalar_type_to_expr_cases(char), \
> __scalar_type_to_expr_cases(short), \
> __scalar_type_to_expr_cases(int), \
> __scalar_type_to_expr_cases(long), \
> __scalar_type_to_expr_cases(long long), \
> default: (x)))
> ```
>
> from [include/linux/compiler_types.h](https://elixir.bootlin.com/linux/v6.15-rc4/source/include/linux/compiler_types.h#L512). I am not that familiar with `_Generic`, does this expression result in a `const` type?
Er, yes and no are both correct answers. If passed an integer scalar, then it results in an unqualified type. If passed a non-integer scalar, it results in whatever you get from `typeof(x)`: https://godbolt.org/z/d8WMvxsdf
To always strip the qualifier, you'd change the macro slightly:
```
#define __unqual_scalar_typeof(x) typeof( \
_Generic((x), \
char: (char)0, \
__scalar_type_to_expr_cases(char), \
__scalar_type_to_expr_cases(short), \
__scalar_type_to_expr_cases(int), \
__scalar_type_to_expr_cases(long), \
__scalar_type_to_expr_cases(long long), \
default: (+x)))
```
https://godbolt.org/z/vTsofhvoo
That forces the rvalue conversion on `x` which strips the qualifiers so that `typeof` gets the unqualified type. Or in C23+ you could use `typeof_unqual` instead.
> > Does adding `= {};` to the declarations for the dummy variable work for you?
>
> Yes, that appears to work to silence the warning but I have not looked to see if this affects code generation yet. I would assume that it would not because the results of the comparison is always dead but I could see things such as sanitizers messing up those optimizations.
>
> > That looks like a correct diagnostic to me, but I admit the code is dense enough I may be missing something. Is there a reason why `addr` should not be initialized? Or is this relying on initialization through other means like `memcpy` into the field because the containing object is non-const?
>
> Yes, this code is definitely dense... My basic understanding is that initialization and modification of `addr` is supposed to happen through `__addr` within the internal crypto API. I see a similar situation with `vm_flags` within `struct vm_area_struct`.
>
> ```
> fs/hugetlbfs/inode.c:733:24: error: default initialization of an object of type 'struct vm_area_struct' with const member leaves the object uninitialized and is incompatible with C++ [-Werror,-Wdefault-const-init-unsafe]
> 733 | struct vm_area_struct pseudo_vma;
> | ^
> include/linux/mm_types.h:803:20: note: member 'vm_flags' declared 'const' here
> 803 | const vm_flags_t vm_flags;
> | ^
> 1 error generated.
> ```
>
> ```c
> struct vm_area_struct {
> ...
> /*
> * Flags, see mm.h.
> * To modify use vm_flags_{init|reset|set|clear|mod} functions.
> */
> union {
> const vm_flags_t vm_flags;
> vm_flags_t __private __vm_flags;
> };
> ...
> };
>
> static long hugetlbfs_fallocate(struct file *file, int mode, loff_t offset,
> loff_t len)
> {
> ...
> struct vm_area_struct pseudo_vma;
> ...
> vma_init(&pseudo_vma, mm);
> vm_flags_init(&pseudo_vma, VM_HUGETLB | VM_MAYSHARE | VM_SHARED);
> ...
> }
>
> static inline void vm_flags_init(struct vm_area_struct *vma,
> vm_flags_t flags)
> {
> ACCESS_PRIVATE(vma, __vm_flags) = flags;
> }
>
> /* sparse defines __CHECKER__; see Documentation/dev-tools/sparse.rst */
> #ifdef __CHECKER__
> ...
> # define ACCESS_PRIVATE(p, member) (*((typeof((p)->member) __force *) &(p)->member))
> #else /* __CHECKER__ */
> ...
> # define ACCESS_PRIVATE(p, member) ((p)->member)
> ```
>
> The other few instances that I looked at either used only one or two fields in an aggregate (not the `const` one so full initialization may be seen as unnecessary overhead) or maybe used `memset()`/`memcpy()` to initialize the field like [the one instance I saw in the floppy driver](https://elixir.bootlin.com/linux/v6.15-rc4/source/drivers/block/floppy.c#L3688).
>
> ```
> drivers/block/floppy.c:3691:30: warning: default initialization of an object of type 'struct compat_floppy_struct' with const member leaves the object uninitialized and is incompatible with C++ [-Wdefault-const-init-unsafe]
> 3691 | struct compat_floppy_struct v;
> | ^
> include/linux/fd.h:20:23: note: member 'name' declared 'const' here
> 20 | const compat_caddr_t name;
> | ^
> ```
This case might be reasonable to handle differently, but I'm on the fence too. There's two cases for structure members, broadly:
1) Don't initialize the `const` field, don't ever read the `const` field.
2) Rely on the fact that you can overwrite a `const` if the top-level object was not declared `const`.
In both cases, the code is valid and so the warning is a false positive. In both cases, the code is dangerous and the warning is useful. So I kind of think this is a case where we split the field diagnostic out into its own group. So we'd have `-Wdefault-const-init-field` which covers field initialization cases, and it would be grouped under `-Wdefault-const-init` which covers both fields and variables. WDYT?
https://github.com/llvm/llvm-project/pull/137166
More information about the cfe-commits
mailing list