On Mon, 8 Jun 2026 at 20:22, <[email protected]> wrote:

> From: Sunil Dora <[email protected]>
>
> Many kernels enforce a per-string length limit on argv and envp
> strings passed to execve().  On Linux, MAX_ARG_STRLEN limits each
> string to 32 * PAGE_SIZE (~128KB); Windows limits individual
> environment variables to 32767 characters.  When the gcc driver
> composes a COLLECT_GCC_OPTIONS value that exceeds such a limit,
> execve() of cc1, collect2 or lto-wrapper fails with E2BIG.
>
> When the assembled value would exceed COLLECT_GCC_OPTIONS_MAX_LENGTH
> (default 1024, host-overridable via defaults.h), the driver writes
> the option list to a temporary response file via writeargv() and
> exports "COLLECT_GCC_OPTIONS=@<path>" instead.  collect2,
> lto-wrapper and lto-plugin transparently expand the @file form using
> existing expandargv() infrastructure, so the change is invisible to
> normal builds.
>
> Bootstrapped and regression tested on x86_64-pc-linux-gnu.
>
>         PR driver/111527
>
> gcc/ChangeLog:
>
>         * defaults.h (COLLECT_GCC_OPTIONS_MAX_LENGTH): New macro.
>         * collect-utils.cc (requote_one_arg): New function.
>         (read_collect_gcc_options): New function.
>         * collect-utils.h (read_collect_gcc_options): Declare.
>         * collect2.cc (main): Use read_collect_gcc_options instead
>         of getenv.
>         * doc/invoke.texi (Environment Variables): Document the
>         @file form of COLLECT_GCC_OPTIONS.
>         * gcc.cc (xsetenv_collect_gcc_options): New function.
>         (set_collect_gcc_options): Use xsetenv_collect_gcc_options.
>         * lto-wrapper.cc (run_gcc): Use read_collect_gcc_options
>         instead of getenv.
>
> gcc/testsuite/ChangeLog:
>
>         * gcc.misc-tests/pr111527.exp: New test.
>
> lto-plugin/ChangeLog:
>
>         * lto-plugin.c (append_quoted_to_buf): New function.
>         (read_collect_gcc_options): New function.
>         (onload): Use read_collect_gcc_options instead of getenv.
>
> Signed-off-by: Sunil Dora <[email protected]>
> ---
> v1 -> v2:
>   - Make spill unconditional, drop configure.ac/configure/config.in
> changes
>   - Add COLLECT_GCC_OPTIONS_MAX_LENGTH (default 1024) to defaults.h
>   - Rename to xsetenv_collect_gcc_options(), set env internally
>   - Fix strchr comparison to use != nullptr
>   - Fix NULL to nullptr in collect-utils.cc
>   - Use XRESIZEVAR in lto-plugin.c
>   - Update invoke.texi to be platform neutral
>   - Remove Linux-only guard from test
>
>  gcc/collect-utils.cc                      | 58 +++++++++++++++++
>  gcc/collect-utils.h                       |  2 +-
>  gcc/collect2.cc                           |  6 +-
>  gcc/defaults.h                            |  6 ++
>  gcc/doc/invoke.texi                       |  8 +++
>  gcc/gcc.cc                                | 48 +++++++++++++-
>  gcc/lto-wrapper.cc                        |  4 +-
>  gcc/testsuite/gcc.misc-tests/pr111527.exp | 78 +++++++++++++++++++++++
>  lto-plugin/lto-plugin.c                   | 68 +++++++++++++++++++-
>  9 files changed, 270 insertions(+), 8 deletions(-)
>  create mode 100644 gcc/testsuite/gcc.misc-tests/pr111527.exp
>
> diff --git a/gcc/collect-utils.cc b/gcc/collect-utils.cc
> index ad37e7a4905..b623088c5c5 100644
> --- a/gcc/collect-utils.cc
> +++ b/gcc/collect-utils.cc
> @@ -269,3 +269,61 @@ utils_cleanup (bool from_signal)
>
>    tool_cleanup (from_signal);
>  }
> +
> +/* Append OPT to OB in COLLECT_GCC_OPTIONS shell-quoted form.
> +   *FIRST_P controls the leading separator and is updated.  */
> +
> +static void
> +requote_one_arg (struct obstack *ob, bool *first_p, const char *opt)
> +{
> +  const char *p, *q = opt;
> +  if (!*first_p)
> +    obstack_grow (ob, " ", 1);
> +  obstack_grow (ob, "'", 1);
> +  while ((p = strchr (q, '\'')) != nullptr)
> +    {
> +      obstack_grow (ob, q, p - q);
> +      obstack_grow (ob, "'\\''", 4);
> +      q = ++p;
> +    }
> +  obstack_grow (ob, q, strlen (q));
> +  obstack_grow (ob, "'", 1);
> +  *first_p = false;
> +}
> +
> +/* Return COLLECT_GCC_OPTIONS, expanding an @file reference if present.
> +   Returns nullptr if unset.  Result is owned by an internal cache.  */
> +
> +const char *
> +read_collect_gcc_options (void)
> +{
> +  static char *cached;
> +
> +  const char *raw = getenv ("COLLECT_GCC_OPTIONS");
> +  if (raw == nullptr)
> +    return nullptr;
> +  if (raw[0] != '@')
> +    return raw;
> +
> +  /* Feed "@file" to expandargv via a minimal argv.  */
> +  int argc = 2;
> +  char **argv = XCNEWVEC (char *, 3);
> +  argv[0] = xstrdup (tool_name);
> +  argv[1] = xstrdup (raw);
> +  argv[2] = nullptr;
> +  expandargv (&argc, &argv);
> +
> +  struct obstack ob;
> +  obstack_init (&ob);
> +  bool first = true;
> +  for (int i = 1; i < argc; i++)
> +    requote_one_arg (&ob, &first, argv[i]);
> +  obstack_1grow (&ob, '\0');
> +  char *result = xstrdup ((const char *) XOBFINISH (&ob, char *));
> +  obstack_free (&ob, nullptr);
> +  freeargv (argv);
> +
> +  free (cached);
> +  cached = result;
> +  return cached;
> +}
>

Hello,

I just have a question, there seems to be code duplication with
read_collect_gcc_options in this file and lto-plugin.c. Have you considered
moving them to a header file and including that?

Thanks,
Shivam

diff --git a/gcc/collect-utils.h b/gcc/collect-utils.h
> index 3ed80271fdb..50b4bba9a0b 100644
> --- a/gcc/collect-utils.h
> +++ b/gcc/collect-utils.h
> @@ -33,7 +33,7 @@ extern int collect_wait (const char *, struct pex_obj *);
>  extern void do_wait (const char *, struct pex_obj *);
>  extern void fork_execute (const char *, char **, bool, const char *);
>  extern void utils_cleanup (bool);
> -
> +extern const char *read_collect_gcc_options (void);
>
>  extern bool debug;
>  extern bool verbose;
> diff --git a/gcc/collect2.cc b/gcc/collect2.cc
> index 6985e0b4d15..c7b0ad4321a 100644
> --- a/gcc/collect2.cc
> +++ b/gcc/collect2.cc
> @@ -1008,7 +1008,7 @@ main (int argc, char **argv)
>      /* Now pick up any flags we want early from COLLECT_GCC_OPTIONS
>         The LTO options are passed here as are other options that might
>         be unsuitable for ld (e.g. -save-temps).  */
> -    p = getenv ("COLLECT_GCC_OPTIONS");
> +    p = read_collect_gcc_options ();
>      while (p && *p)
>        {
>         const char *q = extract_string (&p);
> @@ -1206,7 +1206,7 @@ main (int argc, char **argv)
>       AIX support needs to know if -shared has been specified before
>       parsing commandline arguments.  */
>
> -  p = getenv ("COLLECT_GCC_OPTIONS");
> +  p = read_collect_gcc_options ();
>    while (p && *p)
>      {
>        const char *q = extract_string (&p);
> @@ -1599,7 +1599,7 @@ main (int argc, char **argv)
>        fprintf (stderr, "o_file              = %s\n",
>                (o_file ? o_file : "not found"));
>
> -      ptr = getenv ("COLLECT_GCC_OPTIONS");
> +      ptr = read_collect_gcc_options ();
>        if (ptr)
>         fprintf (stderr, "COLLECT_GCC_OPTIONS = %s\n", ptr);
>
> diff --git a/gcc/defaults.h b/gcc/defaults.h
> index 87f710697f0..1b579339451 100644
> --- a/gcc/defaults.h
> +++ b/gcc/defaults.h
> @@ -1463,4 +1463,10 @@ see the files COPYING3 and COPYING.RUNTIME
> respectively.  If not, see
>  typedef TARGET_UNIT target_unit;
>  #endif
>
> +/* Maximum length of COLLECT_GCC_OPTIONS before the driver spills it
> +   to a response file.  Hosts with tighter limits may override this.  */
> +#ifndef COLLECT_GCC_OPTIONS_MAX_LENGTH
> +#define COLLECT_GCC_OPTIONS_MAX_LENGTH 1024
> +#endif
> +
>  #endif  /* ! GCC_DEFAULTS_H */
> diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
> index 339d1d2c97a..d97186715c2 100644
> --- a/gcc/doc/invoke.texi
> +++ b/gcc/doc/invoke.texi
> @@ -37775,6 +37775,14 @@ set and it cannot connect to it.
>
>  This feature is experimental and subject to change or removal without
>  notice.
> +
> +@vindex COLLECT_GCC_OPTIONS
> +@item COLLECT_GCC_OPTIONS
> +Set by the driver and read by @command{collect2}, @command{lto-wrapper},
> +and the LTO linker plugin to pass the driver's option list.  If
> +the list would exceed @code{COLLECT_GCC_OPTIONS_MAX_LENGTH} bytes,
> +the driver writes it to a temporary file and sets this variable to
> +@samp{@@@var{path}} instead.
>  @end table
>
>  @noindent
> diff --git a/gcc/gcc.cc b/gcc/gcc.cc
> index 0ed2cd96be1..b03e84dd9f5 100644
> --- a/gcc/gcc.cc
> +++ b/gcc/gcc.cc
> @@ -5659,6 +5659,52 @@ process_command (unsigned int decoded_options_count,
>    infiles[n_infiles].name = 0;
>  }
>
> +/* Set COLLECT_GCC_OPTIONS in the environment.  If the value would
> +   exceed COLLECT_GCC_OPTIONS_MAX_LENGTH, spill it to a temporary
> +   response file and set the variable to @<path> instead.  */
> +
> +static void
> +xsetenv_collect_gcc_options (char *string)
> +{
> +  if (strlen (string) <= COLLECT_GCC_OPTIONS_MAX_LENGTH)
> +    {
> +      xputenv (string);
> +      return;
> +    }
> +
> +  static const char prefix[] = "COLLECT_GCC_OPTIONS=";
> +  gcc_assert (startswith (string, prefix));
> +
> +  /* parse_options_from_collect_gcc_options expects argc to start
> +     at 1, so push a placeholder argv[0].  */
> +  struct obstack argv_obstack;
> +  obstack_init (&argv_obstack);
> +  obstack_ptr_grow (&argv_obstack, const_cast<char *> (progname));
> +  int argc;
> +  parse_options_from_collect_gcc_options (string + sizeof (prefix) - 1,
> +                                         &argv_obstack, &argc);
> +  char **argv = XOBFINISH (&argv_obstack, char **);
> +
> +  char *temp_file = make_temp_file ("");
> +  FILE *f = fopen (temp_file, "wb");
> +  if (f == nullptr)
> +    fatal_error (input_location,
> +                "cannot open response file %qs: %m", temp_file);
> +  /* writeargv walks until NULL; skip our placeholder argv[0].  */
> +  if (writeargv (argv + 1, f) != 0)
> +    fatal_error (input_location,
> +                "cannot write response file %qs: %m", temp_file);
> +  if (fclose (f) != 0)
> +    fatal_error (input_location,
> +                "cannot close response file %qs: %m", temp_file);
> +
> +  char *env_val = concat (prefix, "@", temp_file, nullptr);
> +  /* Delete on both success and failure unless -save-temps.  */
> +  record_temp_file (temp_file, !save_temps_flag, !save_temps_flag);
> +  obstack_free (&argv_obstack, nullptr);
> +  xputenv (env_val);
> +}
> +
>  /* Store switches not filtered out by %<S in spec in COLLECT_GCC_OPTIONS
>     and place that in the environment.  */
>
> @@ -5737,7 +5783,7 @@ set_collect_gcc_options (void)
>      }
>
>    obstack_grow (&collect_obstack, "\0", 1);
> -  xputenv (XOBFINISH (&collect_obstack, char *));
> +  xsetenv_collect_gcc_options (XOBFINISH (&collect_obstack, char *));
>  }
>
>  /* Process a spec string, accumulating and running commands.  */
> diff --git a/gcc/lto-wrapper.cc b/gcc/lto-wrapper.cc
> index 9610fa5b7a8..9c90377099d 100644
> --- a/gcc/lto-wrapper.cc
> +++ b/gcc/lto-wrapper.cc
> @@ -1418,7 +1418,7 @@ run_gcc (unsigned argc, char *argv[])
>    char *list_option_full = NULL;
>    const char *linker_output = NULL;
>    const char *collect_gcc;
> -  char *collect_gcc_options;
> +  const char *collect_gcc_options;
>    int parallel = 0;
>    int jobserver = 0;
>    bool jobserver_requested = false;
> @@ -1452,7 +1452,7 @@ run_gcc (unsigned argc, char *argv[])
>    if (!collect_gcc)
>      fatal_error (input_location,
>                  "environment variable %<COLLECT_GCC%> must be set");
> -  collect_gcc_options = getenv ("COLLECT_GCC_OPTIONS");
> +  collect_gcc_options = read_collect_gcc_options ();
>    if (!collect_gcc_options)
>      fatal_error (input_location,
>                  "environment variable %<COLLECT_GCC_OPTIONS%> must be
> set");
> diff --git a/gcc/testsuite/gcc.misc-tests/pr111527.exp
> b/gcc/testsuite/gcc.misc-tests/pr111527.exp
> new file mode 100644
> index 00000000000..bf335871996
> --- /dev/null
> +++ b/gcc/testsuite/gcc.misc-tests/pr111527.exp
> @@ -0,0 +1,78 @@
> +# Copyright (C) 2026 Free Software Foundation, Inc.
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 3 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with GCC; see the file COPYING3.  If not see
> +# <http://www.gnu.org/licenses/>.
> +
> +# PR driver/111527 - very long COLLECT_GCC_OPTIONS exceeding
> +# COLLECT_GCC_OPTIONS_MAX_LENGTH.  The driver should spill
> +# the option list to a response file when it would not fit.
> +
> +load_lib gcc-defs.exp
> +load_lib target-supports.exp
> +
> +if { [is_remote host] } {
> +    return
> +}
> +
> +global GCC_UNDER_TEST
> +if { ![info exists GCC_UNDER_TEST] } {
> +    set GCC_UNDER_TEST [find_gcc]
> +}
> +
> +# Use @file rather than additional_flags: DejaGnu collapses very
> +# long flag strings.  The driver still expands @file into argv and
> +# builds COLLECT_GCC_OPTIONS from it, exercising the spill path.
> +set work_dir [pwd]
> +set src [file join $work_dir "pr111527.c"]
> +set rsp [file join $work_dir "pr111527.rsp"]
> +set obj [file join $work_dir "pr111527.o"]
> +
> +set f [open $src w]
> +puts $f "int main (void) { return 0; }"
> +close $f
> +
> +set f [open $rsp w]
> +for { set i 0 } { $i < 50 } { incr i } {
> +    puts $f "-DPR111527_PADDING_$i=1"
> +}
> +close $f
> +
> +# (1) Build must succeed.
> +set cmd "$GCC_UNDER_TEST -c $src -o $obj @$rsp"
> +verbose -log "Test 1: $cmd"
> +set status [remote_exec host $cmd]
> +set rc [lindex $status 0]
> +set out [lindex $status 1]
> +if { $rc == 0 } {
> +    pass "PR111527: build with very long COLLECT_GCC_OPTIONS"
> +} else {
> +    fail "PR111527: build with very long COLLECT_GCC_OPTIONS"
> +    verbose -log "compiler output: $out"
> +}
> +file delete -force $obj
> +
> +# (2) Confirm spill path engaged.
> +set cmd "$GCC_UNDER_TEST -v -c $src -o $obj @$rsp"
> +verbose -log "Test 2: $cmd"
> +set status [remote_exec host $cmd]
> +set out [lindex $status 1]
> +if { [regexp {COLLECT_GCC_OPTIONS=@[^[:space:]]+} $out] } {
> +    pass "PR111527: driver spilled to @file"
> +} else {
> +    fail "PR111527: driver spilled to @file"
> +    verbose -log "compiler output: $out"
> +}
> +
> +file delete -force $obj
> +file delete -force $rsp
> +file delete -force $src
> diff --git a/lto-plugin/lto-plugin.c b/lto-plugin/lto-plugin.c
> index ffa4fe1552f..569ebe539d1 100644
> --- a/lto-plugin/lto-plugin.c
> +++ b/lto-plugin/lto-plugin.c
> @@ -1506,6 +1506,72 @@ negotiate_api_version (void)
>      }
>  }
>
> +/* Append ARG to *BUFP in COLLECT_GCC_OPTIONS shell-quoted form
> +   (each arg in single quotes, embedded ' becomes '\''), growing
> +   the buffer as needed.  */
> +
> +static void
> +append_quoted_to_buf (char **bufp, size_t *posp, size_t *capp,
> +                   const char *arg, bool first)
> +{
> +  /* 4 bytes per char worst case, plus quotes and separator.  */
> +  size_t need = strlen (arg) * 4 + 4;
> +  if (*posp + need + 1 > *capp)
> +    {
> +      *capp = (*posp + need + 1) * 2;
> +      *bufp = XRESIZEVAR (char, *bufp, *capp);
> +    }
> +  char *p = *bufp + *posp;
> +  if (!first)
> +    *p++ = ' ';
> +  *p++ = '\'';
> +  for (const char *q = arg; *q; q++)
> +    {
> +      if (*q == '\'')
> +       {
> +       *p++ = '\''; *p++ = '\\'; *p++ = '\''; *p++ = '\'';
> +       }
> +      else
> +       *p++ = *q;
> +    }
> +  *p++ = '\'';
> +  *posp = p - *bufp;
> +}
> +
> +/* Return COLLECT_GCC_OPTIONS, expanding @file into the shell-quoted
> +   text expected by onload.  Result owned by an internal cache.  */
> +
> +static const char *
> +read_collect_gcc_options (void)
> +{
> +  static char *cached;
> +
> +  const char *raw = getenv ("COLLECT_GCC_OPTIONS");
> +  if (raw == NULL)
> +    return NULL;
> +  if (raw[0] != '@')
> +    return raw;
> +
> +  int argc = 2;
> +  char **argv = (char **) xcalloc (3, sizeof (char *));
> +  argv[0] = xstrdup ("lto-plugin");
> +  argv[1] = xstrdup (raw);
> +  argv[2] = NULL;
> +  expandargv (&argc, &argv);
> +
> +  size_t cap = 256, pos = 0;
> +  char *buf = (char *) xmalloc (cap);
> +  buf[0] = '\0';
> +  for (int i = 1; i < argc; i++)
> +    append_quoted_to_buf (&buf, &pos, &cap, argv[i], i == 1);
> +  buf[pos] = '\0';
> +  freeargv (argv);
> +
> +  free (cached);
> +  cached = buf;
> +  return cached;
> +}
> +
>  /* Called by a linker after loading the plugin. TV is the transfer
> vector. */
>
>  enum ld_plugin_status
> @@ -1617,7 +1683,7 @@ onload (struct ld_plugin_tv *tv)
>              "could not register the all_symbols_read callback");
>      }
>
> -  char *collect_gcc_options = getenv ("COLLECT_GCC_OPTIONS");
> +  const char *collect_gcc_options = read_collect_gcc_options ();
>    if (collect_gcc_options)
>      {
>        /* Support -fno-use-linker-plugin by failing to load the plugin
> --
> 2.43.0
>
>

Reply via email to