Re: [PATCH RESEND2 1/4] parse: initial parsing of __attribute__((format))

Chris Li <[email protected]> Mon, 1 Dec 2025 23:23:04 +0400
Newsgroups org.kernel.vger.linux-sparse
Message-ID <CACePvbVvsAPURh+jfb2Vh8cPsOzuR2HmzuD9j5Gf6GJyD2orng@mail.gmail.com>
Hi Ben,

Thanks for the patch and sorry for the late reply.

Your format attribute series work applies to the sparse-dev tree fine
and "make check" runs fine as well. Thank you so much.

I have some trivial coding style of feedback for you, see the comments
below. Mostly just nitpicks, does not impact the coding behavior. Let
me know if you want to update a new series or I can be lazy and just
apply your current series.

Chris

On Mon, Oct 20, 2025 at 7:39 PM Ben Dooks <[email protected]> wrote:
>
> Add code to parse the __attribute__((format)) used to indicate that
> a variadic function takes a printf-style format string and where
> those are. Save the data in ctype ready for checking when such an
> function is encoutered.
>
> Signed-off-by: Ben Dooks <[email protected]>
> ---
>  parse.c  | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
>  symbol.h |  9 ++++++-
>  2 files changed, 88 insertions(+), 2 deletions(-)
>
> diff --git a/parse.c b/parse.c
> index 3f67451e..af4e5b50 100644
> --- a/parse.c
> +++ b/parse.c
> @@ -86,7 +86,7 @@ static attr_t
>         attribute_cleanup,
>         attribute_designated_init,
>         attribute_transparent_union, ignore_attribute,
> -       attribute_mode, attribute_force;
> +       attribute_mode, attribute_force, attribute_format;
>
>  typedef struct symbol *to_mode_t(struct symbol *);
>
> @@ -121,6 +121,12 @@ static void asm_modifier(struct token *token, unsigned long *mods, unsigned long
>         *mods |= mod;
>  }
>
> +/* the types of formatting from __attribute__((format)) */
> +enum {
> +       FMT_PRINTF = 0,
> +       FMT_SCANF,
> +};
> +
>  static struct symbol_op typedef_op = {
>         .type = KW_MODIFIER,
>         .declarator = storage_specifier,
> @@ -382,6 +388,10 @@ static struct symbol_op attr_force_op = {
>         .attribute = attribute_force,
>  };
>
> +static struct symbol_op attr_format_op = {
> +       .attribute = attribute_format,
> +};
> +
>  static struct symbol_op address_space_op = {
>         .attribute = attribute_address_space,
>  };
> @@ -441,6 +451,16 @@ static struct symbol_op mode_word_op = {
>         .to_mode = to_word_mode
>  };
>
> +static struct symbol_op attr_printf_op = {
> +       .type   = KW_FORMAT,
> +       .class  = FMT_PRINTF,
> +};
> +
> +static struct symbol_op attr_scanf_op = {
> +       .type   = KW_FORMAT,
> +       .class  = FMT_SCANF,
> +};
> +
>  /*
>   * Define the keyword and their effects.
>   * The entries in the 'typedef' and put in NS_TYPEDEF and
> @@ -557,6 +577,9 @@ static struct init_keyword {
>         D("pure",               &attr_fun_op,           .mods = MOD_PURE),
>         A("const",              &attr_fun_op,           .mods = MOD_PURE),
>         D("gnu_inline",         &attr_fun_op,           .mods = MOD_GNU_INLINE),
> +       D("format",             &attr_format_op),
> +       D("printf",             &attr_printf_op),
> +       D("scanf",              &attr_scanf_op),
>
>         /* Modes */
>         D("mode",               &mode_op),
> @@ -1217,6 +1240,60 @@ static struct token *attribute_address_space(struct token *token, struct symbol
>         return token;
>  }
>
> +static int invalid_format_args(long long start, long long at)
> +{
> +       return start < 0 || at < 0 || start > USHRT_MAX || at > USHRT_MAX ||
> +               (start == at && start > 0) ||
> +               (start == 0 && at == 0);
> +}
> +
> +static struct token *attribute_format(struct token *token, struct symbol *attr, struct decl_state *ctx)
> +{
> +       struct expression *args[3];

I notice that you never use the args as an array, e.g. pass "args" to
any function. You always use args[n] as scalar.
In that case, it is better to make each args as individual variables
with proper names. When I read your patch, I need to lookup gcc
document for the format attribute to understand each args. I found
this:

format (archetype, string-index, first-to-check)

I assume that matches your three args. Then just name the expression
"archetype", "stridx", "first2check" something like that. You get the
idea. I just make up the variable name on the spot, you can probably
find a better variable name than I do.

> +       struct symbol *fmt_sym = NULL;
> +
> +       /* expecting format ( type, start, va_args at) */
> +
> +       token = expect(token, '(', "after format attribute");
> +       if (token_type(token) == TOKEN_IDENT)
> +               fmt_sym = lookup_keyword(token->ident, NS_KEYWORD);
> +       if (fmt_sym)
> +               if (!fmt_sym->op || fmt_sym->op->type != KW_FORMAT)

This two if statement can be combined into one if statement with
compound test expression:
if (fmt_sym && (!fmt_sym->op || fmt_sym->op->type != KW_FORMAT))

> +                       fmt_sym = NULL;
> +
> +       token = conditional_expression(token, &args[0]);

See above, give args[0] a proper name that would be more readable.

> +       token = expect(token, ',', "format attribute type");
> +       token = conditional_expression(token, &args[1]);

Same.

> +       token = expect(token, ',', "format attribute type position");
> +       token = conditional_expression(token, &args[2]);

Same.

> +       token = expect(token, ')', "format attribute arg position");
> +
> +       if (!fmt_sym || !args[0] || !args[1] || !args[2]) {
> +               warning(token->pos, "incorrect format attribute");

In such cases, the kernel source code often bails out early to make
the rest of code flater.
e.g.:
                "goto done" here or "return toke";

> +       } else if (fmt_sym->op->class != FMT_PRINTF) {

If you follow the above suggestion. "} else if () {" becomes "if ()".
That looks cleaner

> +               /* skip anything that isn't printf for the moment */
> +               warning(token->pos, "only printf format attribute supported");

You can also bail out or return early here to save the following "else"

> +       } else {

Use the above suggestion to remove one level of indentation.

> +               long long start, at;
> +
> +               start = get_expression_value(args[2]);

args[2] can use the nice variable name if you follow the above suggestion.

> +               at = get_expression_value(args[1]);
> +
> +               if (invalid_format_args(start, at)) {
> +                       warning(token->pos, "bad format positions");

Same here, bail out early can make the function flatter.

> +               } else if (start == 0) {
> +                       /* nothing to do here, is va_list function */
Same here.

> +               } else if (start < at) {
> +                       warning(token->pos, "format cannot be after va_args");
Same.

> +               } else {

This else can be removed if bail out early above.

> +                       ctx->ctype.format.index = at;
> +                       ctx->ctype.format.first = start;
> +               }
> +       }
> +
> +       return token;
> +}
> +
>  static struct symbol *to_QI_mode(struct symbol *ctype)
>  {
>         if (ctype->ctype.base_type != &int_type)
> @@ -3007,6 +3084,8 @@ struct token *external_declaration(struct token *token, struct symbol_list **lis
>
>                 if (!(decl->ctype.modifiers & MOD_STATIC))
>                         decl->ctype.modifiers |= MOD_EXTERN;
> +
> +               base_type->ctype.format = decl->ctype.format;
>         } else if (base_type == &void_ctype && !(decl->ctype.modifiers & MOD_EXTERN)) {
>                 sparse_error(token->pos, "void declaration");
>         }
> diff --git a/symbol.h b/symbol.h
> index 88130c15..0ea46da8 100644
> --- a/symbol.h
> +++ b/symbol.h
> @@ -82,8 +82,9 @@ enum keyword {
>         KW_ASM          = 1 << 5,
>         KW_MODE         = 1 << 6,
>         KW_STATIC       = 1 << 7,
> -     // KW UNUSED      = 1 << 8,
> +       // KW_UNUSED    = 1 << 8.
>         KW_EXACT        = 1 << 9,

Nobody is using KW_UNUSED. Just delete it and let KW_EXACT use 1 << 8.

> +       KW_FORMAT       = 1 << 10,
Move up this to 1 << 9.


>  };
>
>  struct context {
> @@ -95,12 +96,18 @@ extern struct context *alloc_context(void);
>
>  DECLARE_PTR_LIST(context_list, struct context);
>
> +struct attr_format {
> +       unsigned short index;   /* index in argument list for format string */
> +       unsigned short first;   /* where first variadic argument is */
> +};
> +
>  struct ctype {
>         struct symbol *base_type;
>         unsigned long modifiers;
>         unsigned long alignment;
>         struct context_list *contexts;
>         struct ident *as;
> +       struct attr_format format;

The struct attr_format is very small, adding it here should be fine.
Keep in mind that the struct ctype is a very common data structure in
sparse. Most of the ctype symbols will not have the attr_format
declared. We might want to move the non common ctype into ctype
extensions struct and only store a pointer to the extension struct if
not NULL.

Using the extension attribute I need to check NULL first. It will save
memory space for almost all of the common  ctype.
Because the format member is very small, two shorts, a pointer would
be the same size. We can just add it as it is.


Chris