From 8a14ea8a530eaa607b894fbe4c134db2a72761dc Mon Sep 17 00:00:00 2001 From: Michael Tautschnig Date: Tue, 9 Jun 2026 09:12:19 +0000 Subject: [PATCH] ansi-c: support -fms-extensions anonymous tagged struct/union members A *tagged* struct/union used as an unnamed (anonymous) member -- e.g. the Linux kernel's `struct __filename_head;` embedded in struct filename -- was silently dropped by the C front-end in GCC/Clang mode: it contributed no bytes (so sizeof was wrong, breaking the kernel's static_assert(sizeof(struct filename) % 64 == 0)) and its members were not injected (inaccessible by name). This is the second blocker (after __builtin_has_attribute) to compiling real kernel objects with goto-cc. GCC/Clang accept this only with -fms-extensions (without it they ignore the member, matching the existing regression/ansi-c/anonymous_member* tests). So: - config: add ansi_c.ms_extensions (default false); - gcc_mode: set it when -fms-extensions is passed (queried as "fms-extensions" -- goto_cc_cmdlinet stores long options without the leading '-'); - c_typecheck_type: in GCC/Clang mode accept an *untagged* struct/union anonymous member always (C11) and a *tagged* one only under -fms-extensions; otherwise (and for non-aggregate unnamed members) keep the previous "ignore" behaviour. With the flag the member now contributes its size and its fields are injected (reachable by name). Regression test goto-gcc/ms_extensions_anonymous_member checks exact layout via _Static_assert and member access; the existing anonymous_member1/3 (no flag, sizeof==0) confirm the default behaviour is preserved. Co-authored-by: Kiro --- .../ms_extensions_anonymous_member/main.c | 51 ++++++++++++++++++ .../ms_extensions_anonymous_member/test.desc | 17 ++++++ src/ansi-c/c_typecheck_type.cpp | 52 +++++++++---------- src/goto-cc/gcc_mode.cpp | 8 +++ src/util/config.cpp | 1 + src/util/config.h | 2 + 6 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 regression/goto-gcc/ms_extensions_anonymous_member/main.c create mode 100644 regression/goto-gcc/ms_extensions_anonymous_member/test.desc diff --git a/regression/goto-gcc/ms_extensions_anonymous_member/main.c b/regression/goto-gcc/ms_extensions_anonymous_member/main.c new file mode 100644 index 00000000000..ce13f9e94da --- /dev/null +++ b/regression/goto-gcc/ms_extensions_anonymous_member/main.c @@ -0,0 +1,51 @@ +// -fms-extensions: a *tagged* struct/union used as an unnamed (anonymous) +// member. Its members are injected into the enclosing struct and it +// contributes its size. Used pervasively in the Linux kernel (e.g. +// struct __filename_head embedded in struct filename, whose static_asserts +// require the size to be exact). The _Static_asserts here are checked at +// conversion time, so this fails to compile unless the anonymous tagged +// member is laid out correctly. Member types are deliberately fixed-width +// (no pointers, no long) so the expected sizes are identical on 32-bit and +// 64-bit targets. +struct head +{ + int name; + int refcnt; + int aname; +}; + +struct filename +{ + struct head; // anonymous tagged-struct member + char iname[20]; +}; + +_Static_assert(sizeof(struct head) == 12, "head size"); +_Static_assert(sizeof(struct filename) == 32, "filename size"); +_Static_assert( + __builtin_offsetof(struct filename, iname) == 12, + "iname offset"); + +// a tagged union as an anonymous member, too +struct u_outer +{ + union inner + { + int i; + char c[4]; + }; + int tail; +}; +_Static_assert(sizeof(struct u_outer) == 8, "union-anon size"); + +int main(void) +{ + struct filename f; + f.refcnt = 7; // inner field reachable through the anonymous member + f.name = 0; + + struct u_outer u; + u.i = 3; // inner union field reachable + + return f.refcnt + u.i; +} diff --git a/regression/goto-gcc/ms_extensions_anonymous_member/test.desc b/regression/goto-gcc/ms_extensions_anonymous_member/test.desc new file mode 100644 index 00000000000..b49d5786fd2 --- /dev/null +++ b/regression/goto-gcc/ms_extensions_anonymous_member/test.desc @@ -0,0 +1,17 @@ +CORE +main.c +-fms-extensions -c -o main.gb +^EXIT=0$ +^SIGNAL=0$ +-- +^warning: ignoring +^CONVERSION ERROR$ +-- +With -fms-extensions, goto-cc must accept a *tagged* struct/union as an +anonymous member: it contributes its size and injects its members. The +_Static_asserts (checked at conversion) require exact layout, and the +field accesses require member injection. This is the pattern used by the +Linux kernel's struct filename (struct __filename_head). Member sizes are +fixed-width so the layout asserts hold on both 32-bit and 64-bit targets. +Without -fms-extensions the member is ignored (standard GCC), which is +covered by regression/ansi-c/anonymous_member*. diff --git a/src/ansi-c/c_typecheck_type.cpp b/src/ansi-c/c_typecheck_type.cpp index 2362e3966f6..37768e90164 100644 --- a/src/ansi-c/c_typecheck_type.cpp +++ b/src/ansi-c/c_typecheck_type.cpp @@ -986,32 +986,32 @@ void c_typecheck_baset::typecheck_compound_body( } else { - // GCC and Clang ignore anything other than an untagged struct or - // union; we could print a warning, but there isn't any ambiguity in - // semantics here. Printing a warning could elevate this to an error - // when compiling code with goto-cc with -Werror. - // Note that our type checking always creates a struct_tag/union_tag - // type, but only named struct/union types have an ID_tag member. - if( - new_component.type().id() == ID_struct_tag && - follow_tag(to_struct_tag_type(new_component.type())) - .find(ID_tag) - .is_nil()) - { - // ok, anonymous struct - } - else if( - new_component.type().id() == ID_union_tag && - follow_tag(to_union_tag_type(new_component.type())) - .find(ID_tag) - .is_nil()) - { - // ok, anonymous union - } - else - { - continue; - } + // C11 allows an *untagged* struct/union as an anonymous member. + // -fms-extensions (and MSVC) additionally allow a *tagged* + // struct/union as an anonymous member, injecting its members + // and contributing its size -- used throughout the Linux kernel + // (e.g. `struct __filename_head;` embedded in struct filename). + // Without -fms-extensions, GCC/Clang ignore such a member (it + // contributes no size); we preserve that. A non-struct/union + // unnamed member (e.g. a bare `int;`) is always ignored. + const bool is_struct_tag = + new_component.type().id() == ID_struct_tag; + const bool is_union_tag = new_component.type().id() == ID_union_tag; + + bool is_untagged = false; + if(is_struct_tag) + is_untagged = follow_tag(to_struct_tag_type(new_component.type())) + .find(ID_tag) + .is_nil(); + else if(is_union_tag) + is_untagged = follow_tag(to_union_tag_type(new_component.type())) + .find(ID_tag) + .is_nil(); + + if(!is_struct_tag && !is_union_tag) + continue; // not a struct/union: ignore + if(!is_untagged && !config.ansi_c.ms_extensions) + continue; // tagged anonymous member needs -fms-extensions } } diff --git a/src/goto-cc/gcc_mode.cpp b/src/goto-cc/gcc_mode.cpp index d48c4234abb..8dd10b790b9 100644 --- a/src/goto-cc/gcc_mode.cpp +++ b/src/goto-cc/gcc_mode.cpp @@ -584,6 +584,14 @@ int gcc_modet::doit() if(cmdline.isset("-fsingle-precision-constant")) config.ansi_c.single_precision_constant=true; + // -fms-extensions enables MS/GCC extensions such as anonymous members + // that are a *tagged* struct/union type (used throughout the Linux + // kernel, e.g. struct __filename_head embedded in struct filename). + // Note: goto_cc_cmdlinet stores long options without the leading '-', + // so we query "fms-extensions" (cf. the "fshort-double" check below). + if(cmdline.isset("fms-extensions")) + config.ansi_c.ms_extensions = true; + // -fshort-double makes double the same as float if(cmdline.isset("fshort-double")) config.ansi_c.double_width=config.ansi_c.single_width; diff --git a/src/util/config.cpp b/src/util/config.cpp index da70a3b9888..9deba2c4468 100644 --- a/src/util/config.cpp +++ b/src/util/config.cpp @@ -867,6 +867,7 @@ bool configt::set(const cmdlinet &cmdline) cpp.cpp_standard=cppt::default_cpp_standard(); ansi_c.single_precision_constant=false; + ansi_c.ms_extensions = false; ansi_c.for_has_scope=true; // C99 or later ansi_c.ts_18661_3_Floatn_types=false; ansi_c.__float128_is_keyword = false; diff --git a/src/util/config.h b/src/util/config.h index e1cdc3eb99c..fe6cc393d54 100644 --- a/src/util/config.h +++ b/src/util/config.h @@ -168,6 +168,8 @@ class configt bool bf16_type; // __bf16 (Clang >= 15, GCC >= 13) bool fp16_type; // __fp16 (GCC >= 4.5 on ARM, Clang >= 6) bool single_precision_constant; + bool ms_extensions = false; // -fms-extensions: MS/GCC anonymous tagged + // struct/union members, etc. enum class c_standardt { C89,