From 67beeb5621051eda74ca2171ea576c9602bb6cce Mon Sep 17 00:00:00 2001 From: Kees Cook Date: Thu, 3 Sep 2026 16:24:35 -0700 Subject: randstruct: fix container_of() false positives after __mptr removal Commit f9e7a7564834 ("container_of: remove local __mptr variable") dropped the "void *__mptr" temporary from container_of(). The randstruct GCC plugin's find_bad_casts pass recognized the casts container_of() generates by that variable's name: const_tree ssa_name_var = SSA_NAME_VAR(rhs1); /* skip bogus type casts introduced by container_of */ if (ssa_name_var != NULL_TREE && DECL_NAME(ssa_name_var) && !strcmp(DECL_NAME_POINTER(ssa_name_var), "__mptr")) continue; With the variable gone the suppression never fires, so every container_of() whose container type is randomized now emits a note: include/linux/container_of.h:23:9: note: randstruct: casting between randomized structure pointer types (ssa): 'struct ocfs2_triggers' and 'struct jbd2_buffer_trigger_type' fs/ocfs2/journal.c:524:16: note: in expansion of macro 'container_of' The pass runs on GIMPLE, after folding, and for a member at offset 0 the whole expression collapses to a bare copy that is indistinguishable from an unsafe cast: to_ocfs2_trigger (struct jbd2_buffer_trigger_type * triggers) { _2 = triggers_1(D); /* void * cast and subtraction gone */ return _2; } Match the type relationship instead. A cast from A * to B * is a container_of() if B reaches a field of type A at offset 0 through a chain of by-value members. The chain matters: container_of()'s member argument may be a dotted path, as in ceph_inode(), which is container_of(inode, struct ceph_inode_info, netfs.inode) and needs two levels. The search is depth-bounded to 4 just in case, since real paths are generally one or two members deep. Requiring the cast happens at offset 0 is done because any other member offset the subtraction survives folding and the cast's rhs is still void *, which the pass already skips a few lines above. A cast between two randomized types with no containment relationship is still reported. Verified with: struct cred *f(struct file *f) { return (struct cred *)f; } which is still flagged with the patch applied. Clang's implementation is unaffected. It checks the cast as written, and both the old and new macros cast from void *, which is always permitted; a genuinely bad cast is rejected there as a hard error rather than a note. Build tested ARCH=x86_64 defconfig with CONFIG_RANDSTRUCT_FULL=y and GCC 14.2.0: randstruct notes 52 before, 0 after. Fixes: f9e7a7564834 ("container_of: remove local __mptr variable") Assisted-by: Claude:claude-opus-5[1m] Link: https://patch.msgid.link/20260903232438.60394-1-kees@kernel.org Signed-off-by: Kees Cook --- scripts/gcc-plugins/randomize_layout_plugin.c | 61 +++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/gcc-plugins/randomize_layout_plugin.c b/scripts/gcc-plugins/randomize_layout_plugin.c index ff65a4f87f24..e2bd9ba08089 100644 --- a/scripts/gcc-plugins/randomize_layout_plugin.c +++ b/scripts/gcc-plugins/randomize_layout_plugin.c @@ -698,6 +698,63 @@ static void handle_local_var_initializers(void) } } +/* + * Does @container reach a field of type @member_type by a chain of + * by-value members? That is the relationship container_of() expresses -- + * its @member argument may be a dotted path, e.g. + * container_of(inode, struct ceph_inode_info, netfs.inode) -- so a cast + * from @member_type * to @container * is legitimate rather than a + * layout-confusing one. + * + * container_of() used to leave a "void *__mptr" temporary behind, and this + * pass recognised such casts by that name. Commit f9e7a7564834 + * ("container_of: remove local __mptr variable") removed it to stop nested + * container_of() shadowing itself, and the cast now folds to a bare SSA + * copy when the member sits at offset 0, leaving nothing syntactic to key + * on. Match the type relationship instead. + * + * The depth bound keeps this cheap; container_of() paths are one or two + * members deep in practice. + */ +#define CONTAINER_OF_MAX_DEPTH 4 + +static bool is_container_of_cast(const_tree container, const_tree member_type, + int depth) +{ + const_tree field; + + if (container == NULL_TREE || depth > CONTAINER_OF_MAX_DEPTH) + return false; + + if (TREE_CODE(container) != RECORD_TYPE && + TREE_CODE(container) != UNION_TYPE) + return false; + + for (field = TYPE_FIELDS(container); field; field = DECL_CHAIN(field)) { + const_tree field_type; + + if (TREE_CODE(field) != FIELD_DECL) + continue; + + /* + * Only a member at offset 0 can reach here: for any other + * offset container_of()'s subtraction survives folding, the + * cast's rhs stays void *, and the caller skipped it above. + */ + if (!integer_zerop(byte_position(field))) + continue; + + field_type = TYPE_MAIN_VARIANT(TREE_TYPE(field)); + if (field_type == member_type) + return true; + + if (is_container_of_cast(field_type, member_type, depth + 1)) + return true; + } + + return false; +} + /* * iterate over all statements to find "bad" casts: * those where the address of the start of a structure is cast @@ -799,10 +856,8 @@ static unsigned int find_bad_casts_execute(void) #endif MISMATCH(gimple_location(stmt), "op0", ptr_lhs_type, op0_type); } else { - const_tree ssa_name_var = SSA_NAME_VAR(rhs1); /* skip bogus type casts introduced by container_of */ - if (ssa_name_var != NULL_TREE && DECL_NAME(ssa_name_var) && - !strcmp((const char *)DECL_NAME_POINTER(ssa_name_var), "__mptr")) + if (is_container_of_cast(ptr_lhs_type, ptr_rhs_type, 0)) continue; #ifndef __DEBUG_PLUGIN if (lookup_attribute("randomize_performed", TYPE_ATTRIBUTES(ptr_rhs_type))) -- cgit v1.2.3 From b9f13e51ad1262dd77b1178c8011153894765c7b Mon Sep 17 00:00:00 2001 From: Kees Cook Date: Thu, 3 Sep 2026 16:24:36 -0700 Subject: randstruct: report bad casts as warnings rather than notes find_bad_casts() reports a cast between two randomized structure pointer types with inform(), which renders as a "note:". It has done so since the plugin was originally added, while using error() freely for attribute misuse, UAPI structs, and version mismatches. Clang's implementation of the same check has always been stricter: it rejects such a cast as a full error. There is no reason for the GCC side to be effectively silent about the same problem. Build tested ARCH=x86_64 with CONFIG_RANDSTRUCT_FULL=y and GCC 14.2.0: allmodconfig clean, and defconfig clean under three different random seeds. A deliberate bad cast is still reported, now as a warning, at the correct line and column. Link: https://patch.msgid.link/20260903232438.60394-2-kees@kernel.org Signed-off-by: Kees Cook --- scripts/gcc-plugins/randomize_layout_plugin.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/gcc-plugins/randomize_layout_plugin.c b/scripts/gcc-plugins/randomize_layout_plugin.c index e2bd9ba08089..1e66f45fe29b 100644 --- a/scripts/gcc-plugins/randomize_layout_plugin.c +++ b/scripts/gcc-plugins/randomize_layout_plugin.c @@ -22,7 +22,7 @@ #define ORIG_TYPE_NAME(node) \ (TYPE_NAME(TYPE_MAIN_VARIANT(node)) != NULL_TREE ? ((const unsigned char *)IDENTIFIER_POINTER(TYPE_NAME(TYPE_MAIN_VARIANT(node)))) : (const unsigned char *)"anonymous") -#define INFORM(loc, msg, ...) inform(loc, "randstruct: " msg, ##__VA_ARGS__) +#define INFORM(loc, msg, ...) warning_at(loc, 0, "randstruct: " msg, ##__VA_ARGS__) #define MISMATCH(loc, how, ...) INFORM(loc, "casting between randomized structure pointer types (" how "): %qT and %qT\n", __VA_ARGS__) __visible int plugin_is_GPL_compatible; -- cgit v1.2.3 From 6e72cefa7a47f33fac62a69b22e02b379b7a754b Mon Sep 17 00:00:00 2001 From: Kees Cook Date: Mon, 14 Sep 2026 15:37:49 -0700 Subject: coccinelle: kmalloc_obj: Convert multi-byte integral allocations Currently the kmalloc_obj conversion rules leave alone every allocation sized to an integral type, or assigned to a pointer to one, so that byte buffers keep being allocated with a byte count. That also leaves alone arrays of multi-byte integers, such as vals = kcalloc(n, sizeof(u32), GFP_KERNEL); which count objects like any other array. At the same time the signed types (s8 through s64 and their variants) were not in the list at all. But these multi-byte types are better handled through the full kmalloc_obj-style allocation, as they are, in fact, typed and aren't just a byte string, so they would benefit from type checking. Split INTEGRAL into BYTE_TYPES and MULTIBYTE_TYPES: - Allocations sized by a byte type or a string literal, or assigned to a pointer to a byte type, are still left alone. - Allocations sized by a multi-byte type are converted when they are assigned to a pointer, or a pointer to const, of that same type. Other allocations sized by a multi-byte type are left alone, as their target may be a pointer to an array. - Allocations of other types assigned to a pointer to a multi-byte type are still left alone. - Arrays of pointers to integral types sized by type, such as sizeof(char *), are still left alone, now including the signed types, as their target may be a pointer to an array of pointers, like "const char *(*fmts)[]". Sized as sizeof(*target), they are converted like any other array. List the signed types and the other spellings of the standard integer types (short int, unsigned, long long int, and so on), so that a declaration and a sizeof() that spell the same type differently still match. Assisted-by: LLM coccinelle Link: https://patch.msgid.link/20260914223748.i.971-kees@kernel.org Signed-off-by: Kees Cook --- scripts/coccinelle/api/kmalloc_objs.cocci | 161 ++++++++++++++++++++++++------ 1 file changed, 129 insertions(+), 32 deletions(-) (limited to 'scripts') diff --git a/scripts/coccinelle/api/kmalloc_objs.cocci b/scripts/coccinelle/api/kmalloc_objs.cocci index e9a415b7b6f4..0a98ddf03eca 100644 --- a/scripts/coccinelle/api/kmalloc_objs.cocci +++ b/scripts/coccinelle/api/kmalloc_objs.cocci @@ -24,27 +24,57 @@ def alloc_array(name): print(f"Unknown transform for {name}", file=sys.stderr) return func -// This excludes anything that is assigning to or from integral types or -// string literals. Everything else gets the sizeof() extracted for the -// kmalloc_obj() type/var argument. sizeof(void *) is also excluded because -// it will need case-by-case double-checking to make sure the right type is +// Allocations sized by a byte-sized type (BYTE_TYPES) or a string literal, +// and allocations assigned to a pointer to a byte-sized type, are byte +// buffers and are left alone. sizeof(void *) is also excluded because it +// will need case-by-case double-checking to make sure the right type is // being assigned. +// +// Allocations sized by a multi-byte integral type (MULTIBYTE_TYPES) are +// converted when they are assigned to a pointer to that same type. +// Otherwise they are left alone: the target may be a pointer to an array +// of that type, such as "s16 (*pairs)[2]", for which the converted +// allocation would have the wrong pointer type. For the same reason, +// arrays of pointers to integral types sized as sizeof(char *) and the +// like are left alone. Allocations of other types assigned to a pointer to +// a multi-byte integral type are left alone too. +// +// Everything else gets the sizeof() extracted for the kmalloc_obj() +// type/var argument. +// +// The first matching alternative below wins, so the exclusions must come +// before the more general conversions. @direct depends on patch && !(file in "tools") && !(file in "samples")@ typedef u8, u16, u32, u64; typedef __u8, __u16, __u32, __u64; typedef uint8_t, uint16_t, uint32_t, uint64_t; +typedef s8, s16, s32, s64; +typedef __s8, __s16, __s32, __s64; +typedef int8_t, int16_t, int32_t, int64_t; typedef uchar, ushort, uint, ulong; typedef __le16, __le32, __le64; typedef __be16, __be32, __be64; typedef wchar_t; -type INTEGRAL = {u8,__u8,uint8_t,char,unsigned char,uchar,wchar_t, - u16,__u16,uint16_t,unsigned short,ushort, - u32,__u32,uint32_t,unsigned int,uint, - u64,__u64,uint64_t,unsigned long,ulong, - __le16,__le32,__le64,__be16,__be32,__be64}; +type BYTE_TYPES = {char,signed char,unsigned char,uchar, + u8,__u8,uint8_t,s8,__s8,int8_t}; +type MULTIBYTE_TYPES = {short,short int,signed short,signed short int, + unsigned short,unsigned short int,ushort, + int,signed,signed int,unsigned,unsigned int,uint, + long,long int,signed long,signed long int, + unsigned long,unsigned long int,ulong, + long long,long long int, + signed long long,signed long long int, + unsigned long long,unsigned long long int, + u16,__u16,uint16_t,s16,__s16,int16_t, + u32,__u32,uint32_t,s32,__s32,int32_t, + u64,__u64,uint64_t,s64,__s64,int64_t, + __le16,__le32,__le64,__be16,__be32,__be64, + wchar_t}; char [] STRING; -INTEGRAL *BYTES; -INTEGRAL **BYTES_PTRS; +BYTE_TYPES *BYTES; +MULTIBYTE_TYPES *MULTIBYTES; +const MULTIBYTE_TYPES *CONST_MULTIBYTES; +MULTIBYTE_TYPES MULTIBYTE; type TYPE; expression VAR; expression GFP; @@ -59,66 +89,133 @@ fresh identifier ALLOC_OBJS = script:python(ALLOC_ARRAY) { alloc_array(ALLOC_ARR @@ ( +// Convert a single object sized by its target: p = kmalloc(sizeof(*p), gfp) - VAR = ALLOC((sizeof(*VAR)), GFP) + VAR = ALLOC_OBJ(*VAR, GFP) | - ALLOC((\(sizeof(STRING)\|sizeof(INTEGRAL)\|sizeof(INTEGRAL *)\)), GFP) +// Exclude byte buffers and integral pointers: kmalloc(sizeof(u8), gfp), +// kmalloc(sizeof("str"), gfp), kmalloc(sizeof(char *), gfp) + ALLOC((\(sizeof(STRING)\|sizeof(BYTE_TYPES)\| + sizeof(BYTE_TYPES *)\|sizeof(MULTIBYTE_TYPES *)\)), GFP) | - BYTES = ALLOC((sizeof(E)), GFP) +// Exclude anything assigned to a byte pointer: +// u8 *buf = kmalloc(sizeof(*hdr), gfp) + BYTES = ALLOC((\(sizeof(E)\|sizeof(TYPE)\)), GFP) | - BYTES = ALLOC((sizeof(TYPE)), GFP) +// Convert a multi-byte type to a pointer to it: +// u32 *p = kmalloc(sizeof(u32), gfp) + \(MULTIBYTES\|CONST_MULTIBYTES\) = +- ALLOC((sizeof(MULTIBYTE_TYPES)), GFP) ++ ALLOC_OBJ(MULTIBYTE_TYPES, GFP) | - BYTES_PTRS = ALLOC((sizeof(E)), GFP) +// Same by expression: u32 *p = kmalloc(sizeof(p[0]), gfp) + \(MULTIBYTES\|CONST_MULTIBYTES\) = +- ALLOC((sizeof(MULTIBYTE)), GFP) ++ ALLOC_OBJ(MULTIBYTE, GFP) | - BYTES_PTRS = ALLOC((sizeof(TYPE)), GFP) +// Exclude other multi-byte sizes, e.g. to pointers to arrays: s16 (*p)[2] = ... + ALLOC((\(sizeof(MULTIBYTE_TYPES)\|sizeof(MULTIBYTE)\)), GFP) | +// Exclude anything else assigned to a multi-byte pointer: +// u32 *p = kmalloc(sizeof(*hdr), gfp) + \(MULTIBYTES\|CONST_MULTIBYTES\) = ALLOC((\(sizeof(E)\|sizeof(TYPE)\)), GFP) +| +// Exclude void pointers, to be checked by hand: kmalloc(sizeof(void *), gfp) ALLOC((sizeof(void *)), GFP) | +// Convert any other expression: p = kmalloc(sizeof(s->item), gfp) - ALLOC((sizeof(E)), GFP) + ALLOC_OBJ(E, GFP) | +// Convert any other type: p = kmalloc(sizeof(struct item), gfp) - ALLOC((sizeof(TYPE)), GFP) + ALLOC_OBJ(TYPE, GFP) | - ALLOC_ARRAY(COUNT, (\(sizeof(STRING)\|sizeof(INTEGRAL)\|sizeof(INTEGRAL *)\)), GFP) -| - BYTES = ALLOC_ARRAY(COUNT, (sizeof(E)), GFP) -| - BYTES = ALLOC_ARRAY(COUNT, (sizeof(TYPE)), GFP) -| - BYTES_PTRS = ALLOC_ARRAY(COUNT, (sizeof(E)), GFP) +// The same, for arrays allocated as (count, size): +// Exclude byte buffers and integral pointers: kcalloc(n, sizeof(u8), gfp), +// kcalloc(n, sizeof(char *), gfp) + ALLOC_ARRAY(COUNT, (\(sizeof(STRING)\|sizeof(BYTE_TYPES)\| + sizeof(BYTE_TYPES *)\|sizeof(MULTIBYTE_TYPES *)\)), GFP) | - BYTES_PTRS = ALLOC_ARRAY(COUNT, (sizeof(TYPE)), GFP) +// Exclude arrays assigned to a byte pointer: +// u8 *buf = kcalloc(n, sizeof(*hdr), gfp) + BYTES = ALLOC_ARRAY(COUNT, (\(sizeof(E)\|sizeof(TYPE)\)), GFP) | - ALLOC_ARRAY((\(sizeof(STRING)\|sizeof(INTEGRAL)\|sizeof(INTEGRAL *)\)), COUNT, GFP) +// Convert a multi-byte array to a pointer to it: +// u32 *p = kcalloc(n, sizeof(u32), gfp) + \(MULTIBYTES\|CONST_MULTIBYTES\) = +- ALLOC_ARRAY(COUNT, (sizeof(MULTIBYTE_TYPES)), GFP) ++ ALLOC_OBJS(MULTIBYTE_TYPES, COUNT, GFP) | - BYTES = ALLOC_ARRAY((sizeof(E)), COUNT, GFP) +// Same by expression: u32 *p = kcalloc(n, sizeof(*p), gfp) + \(MULTIBYTES\|CONST_MULTIBYTES\) = +- ALLOC_ARRAY(COUNT, (sizeof(MULTIBYTE)), GFP) ++ ALLOC_OBJS(MULTIBYTE, COUNT, GFP) | - BYTES = ALLOC_ARRAY((sizeof(TYPE)), COUNT, GFP) +// Exclude other multi-byte arrays: +// s16 (*pairs)[2] = kcalloc(n, sizeof(s16), gfp) + ALLOC_ARRAY(COUNT, (\(sizeof(MULTIBYTE_TYPES)\|sizeof(MULTIBYTE)\)), GFP) | - BYTES_PTRS = ALLOC_ARRAY((sizeof(E)), COUNT, GFP) -| - BYTES_PTRS = ALLOC_ARRAY((sizeof(TYPE)), COUNT, GFP) +// Exclude other arrays assigned to a multi-byte pointer: +// u32 *p = kcalloc(n, sizeof(*hdr), gfp) + \(MULTIBYTES\|CONST_MULTIBYTES\) = ALLOC_ARRAY(COUNT, (\(sizeof(E)\|sizeof(TYPE)\)), GFP) | +// Exclude arrays of void pointers: kcalloc(n, sizeof(void *), gfp) ALLOC_ARRAY(COUNT, (sizeof(void *)), GFP) | - ALLOC_ARRAY((sizeof(void *)), COUNT, GFP) -| +// Convert any other expression: p = kcalloc(n, sizeof(*p), gfp) - ALLOC_ARRAY(COUNT, (sizeof(E)), GFP) + ALLOC_OBJS(E, COUNT, GFP) | +// Convert any other type: p = kcalloc(n, sizeof(struct item), gfp) - ALLOC_ARRAY(COUNT, (sizeof(TYPE)), GFP) + ALLOC_OBJS(TYPE, COUNT, GFP) | +// The same, for arrays allocated as (size, count): +// Exclude byte buffers and integral pointers: kcalloc(sizeof(u8), n, gfp), +// kcalloc(sizeof(char *), n, gfp) + ALLOC_ARRAY((\(sizeof(STRING)\|sizeof(BYTE_TYPES)\| + sizeof(BYTE_TYPES *)\|sizeof(MULTIBYTE_TYPES *)\)), COUNT, GFP) +| +// Exclude arrays assigned to a byte pointer: +// u8 *buf = kcalloc(sizeof(*hdr), n, gfp) + BYTES = ALLOC_ARRAY((\(sizeof(E)\|sizeof(TYPE)\)), COUNT, GFP) +| +// Convert a multi-byte array to a pointer to it: +// u32 *p = kcalloc(sizeof(u32), n, gfp) + \(MULTIBYTES\|CONST_MULTIBYTES\) = +- ALLOC_ARRAY((sizeof(MULTIBYTE_TYPES)), COUNT, GFP) ++ ALLOC_OBJS(MULTIBYTE_TYPES, COUNT, GFP) +| +// Same by expression: u32 *p = kcalloc(sizeof(*p), n, gfp) + \(MULTIBYTES\|CONST_MULTIBYTES\) = +- ALLOC_ARRAY((sizeof(MULTIBYTE)), COUNT, GFP) ++ ALLOC_OBJS(MULTIBYTE, COUNT, GFP) +| +// Exclude other multi-byte arrays: +// s16 (*pairs)[2] = kcalloc(sizeof(s16), n, gfp) + ALLOC_ARRAY((\(sizeof(MULTIBYTE_TYPES)\|sizeof(MULTIBYTE)\)), COUNT, GFP) +| +// Exclude other arrays assigned to a multi-byte pointer: +// u32 *p = kcalloc(sizeof(*hdr), n, gfp) + \(MULTIBYTES\|CONST_MULTIBYTES\) = ALLOC_ARRAY((\(sizeof(E)\|sizeof(TYPE)\)), COUNT, GFP) +| +// Exclude arrays of void pointers: kcalloc(sizeof(void *), n, gfp) + ALLOC_ARRAY((sizeof(void *)), COUNT, GFP) +| +// Convert any other expression: p = kcalloc(sizeof(*p), n, gfp) - ALLOC_ARRAY((sizeof(E)), COUNT, GFP) + ALLOC_OBJS(E, COUNT, GFP) | +// Convert any other type: p = kcalloc(sizeof(struct item), n, gfp) - ALLOC_ARRAY((sizeof(TYPE)), COUNT, GFP) + ALLOC_OBJS(TYPE, COUNT, GFP) | +// Convert flexible array structures: p = kmalloc(struct_size(p, data, n), gfp) - ALLOC(struct_size(VAR, FLEX, COUNT), GFP) + ALLOC_FLEX(*VAR, FLEX, COUNT, GFP) | +// Same by type: kmalloc(struct_size_t(struct item, data, n), gfp) - ALLOC(struct_size_t(TYPE, FLEX, COUNT), GFP) + ALLOC_FLEX(TYPE, FLEX, COUNT, GFP) ) -- cgit v1.2.3