April 26, 2023·6 min read·engineeringcgccc23memory-safety

GCC 13 gives trailing arrays an explicit interpretation

GCC 13 adds C23 declarations and a flexible-array policy that distinguishes standard variable tails from historical structure layouts.

A trailing array with one element can mean two different things to a C programmer reading an old structure definition. It can be an actual one-element array, or it can be a historical marker for storage allocated beyond the structure. Those intentions require different bounds reasoning. GCC 13 adds an option that lets a build specify which declarations the compiler should treat as flexible arrays.

The GCC 13 release page dates the first release to April 26. Its change notes introduce -fstrict-flex-arrays alongside substantial C23 support. The array option concerns existing source and how the compiler interprets access to storage. It deserves attention even in a project that has no immediate reason to adopt C23 syntax.

I like C's direct expression of storage layouts, but that benefit depends on declarations describing the layout's actual rules. A fixed bound used as a convention for an unbounded tail makes the same syntax carry conflicting meanings. Giving the compiler a deliberate interpretation is a useful step toward making those meanings explicit.

The historical convention affects bounds

The change notes say GCC normally treats all trailing arrays in aggregates as flexible array members. This accommodates source written using historical conventions. It also means a numeric bound at the end of a structure does not necessarily receive the same treatment as a numeric bound elsewhere.

GCC 13 exposes that choice through -fstrict-flex-arrays. The GCC 13.1 C dialect manual defines the strict form as recognizing only a flexible array member declared according to the C99 rules. Its positive form is equivalent to level 3. Conversely, the negative form is equivalent to level 0, which preserves the least restrictive interpretation.

Two declarations make the policy difference visible. Each places a byte array after a length field.

struct variable_message {
    unsigned length;
    unsigned char payload[];
};

struct fixed_message {
    unsigned length;
    unsigned char payload[1];
};

With the strict interpretation, variable_message.payload is a flexible array member. The one-element declaration in fixed_message remains a fixed array. These declarations now express different contracts to the compiler even though both arrays appear last in their structures.

That distinction is about the declared member. It does not calculate an allocation size or check that a particular allocation contains enough bytes. A length field also does not, by its presence alone, establish that storage corresponding to that length exists. The option supplies a clearer starting point for compiler reasoning about the member's declared bounds.

This matters for maintenance because a historical tail convention is often embedded in a public structure. Replacing a declaration requires attention to the surrounding representation and its users. Merely enabling stricter interpretation cannot transform a fixed array into a standard flexible array member. The source declaration and the compiler policy need to agree about the intended access.

Diagnostics follow the selected policy

The release notes connect this change to -Warray-bounds=2. That warning level no longer independently warns about out-of-bounds access to trailing one-element arrays. Instead, GCC diagnoses trailing-array access according to the flexible-array policy.

That moves a decision out of a warning-level convention and into an explicit interpretation of the source. A project can request bounds warnings while separately stating whether a one-element trailing array represents flexible storage. This is more precise than assuming that a stronger warning level necessarily means every historical layout convention is rejected.

A syntax and warning check can name the policy explicitly. The following command applies the strict interpretation to a source file.

gcc -std=c17 -fstrict-flex-arrays=3 -Warray-bounds=2 -fsyntax-only message.c

The command deliberately selects an existing language mode. Flexible-array interpretation is useful independently of adopting the new draft language. The exact warnings still depend on the accesses in the file and the compiler's available information, so the flag is not a promise to prove every access safe.

GCC also documents a strict_flex_array attribute for individual trailing array fields. That provides a way to describe a local policy where an entire codebase cannot use one interpretation. It is especially relevant when source contains structures written under different conventions, although introducing an attribute still requires a decision about the intended layout.

For me, the benefit is that an old compatibility accommodation becomes a visible build choice. A reader can distinguish a project that intentionally retains broad trailing-array treatment from one that expects standard flexible members. That information belongs with compiler settings because it affects how the same declaration participates in bounds analysis.

A fixed member's bound is useful information even when most code never indexes beyond it. Treating every final array as flexible weakens the distinction between the fixed and variable declarations. The strict policy restores that distinction for the compiler, while the broad policy preserves compatibility with source that used fixed syntax to describe variable storage.

The difference also explains why the option has several levels instead of a single global prohibition. A codebase can contain declarations written under more than one convention. GCC documents a policy mechanism because the compiler cannot infer the author's intended convention from the final array's position alone. Choosing that policy is a statement about the source being compiled.

A local attribute can make that statement narrower than a complete build setting. This does not remove the maintenance problem, since someone still has to establish which convention each structure uses. It does allow the source to record a decision at the field where the ambiguity originates. That is useful information for both compiler diagnostics and later readers of the structure definition.

C23 adds new ways to express declarations

The language additions have a related concern with explicit type information. GCC 13 implements the nullptr constant under N3042, type inference for object definitions under N3007, and the constexpr specifier for object definitions under N3018. The notes also list fixed underlying enumeration types and storage-class specifiers for compound literals.

These are separate facilities with separate purposes. Type inference derives a declaration's type from its initializer. A fixed underlying enumeration type states a representation choice. A constant object declaration expresses a different requirement again. Listing them under one draft-standard heading does not make them interchangeable ways to shorten source.

The release also supports typeof and typeof_unqual. The first spelling had already existed as an extension, while the C23 work supplies standard-language treatment and an unqualified form. For generic source, that distinction lets a declaration preserve a type or explicitly request an unqualified result.

GCC's change notes identify -std=c2x when discussing conformity and pedantic diagnostics for these additions. That mode selection is part of using the draft facilities. Installing GCC 13 does not rewrite the language mode selected by a project's build files, and a project's portable subset remains determined by the compilers and modes it supports.

The same release brings keyword spellings such as bool, static_assert, and thread_local into its C23 work. It also lists removals, including trigraphs and unprototyped functions. A new standard mode therefore changes accepted historical constructs as well as adding declarations that older modes lack.

Analysis also gains resource semantics

GCC 13's static analyzer gains checks for file-descriptor misuse. The change notes list errors such as double close, use after close, leaks, and inappropriate operations for a descriptor's access mode. They also describe function attributes that identify descriptor arguments and whether a function reads or writes through them.

These annotations provide information that an integer parameter alone does not express. A descriptor may have integer representation while carrying resource state that depends on earlier calls. The analyzer's special handling of functions such as open, close, read, and write helps connect that state across operations.

Variadic argument handling gains related checks. The analyzer can report missing va_end, use after va_end, mismatched va_arg types, and exhaustion of a list along interprocedural execution paths. These checks concern obligations that are easy to obscure behind macros and helper functions.

The release notes explicitly restrict expectations by describing -fanalyzer as suitable for C and unlikely to produce meaningful results for C++. That qualification matters when a single compiler release serves both languages. The C analysis work should be assessed on its documented terms.

My interest in these changes comes from the gap between a small language and the obligations that real systems impose on it. A trailing array has an allocation contract. Descriptors have resource lifecycles. Variadic lists require a sequence and cleanup rule. GCC 13 gives the compiler more explicit information about several of those obligations without changing their existence.

For a codebase with historical trailing arrays, the immediate decision is which declarations truly represent variable storage. Strict interpretation can then make a one-element member mean one element. Keeping a flexible member for an actual variable tail lets its declaration carry the intended distinction into the compiler's bounds reasoning.