On Mon, Jun 8, 2026 at 7:50 AM <[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);
I was thinking we could just call buildargv with the buffer of the
file but that only handles one level deep. But expandargv handles
multiple levels. Though in theory we could have multiple levels I
guess if the file is created by hand rather than the passing from the
driver.
> +
> + 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);
I have not looked here if there will be some memory leaked; I suspect
there might be but I could be wrong.
> +
> + free (cached);
> + cached = result;
> + return cached;
> +}
> 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
This should be documented in the internals manual in hostconfig.texi.
And the name should be COLLECT2_... like the other host macros
controlling collect2 there.
> +
> #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,
Don't reference the internal host macro here since this is user
documentation rather than an internals manual.
Something like:
If the list is too big (defaults to 1k), ...
> +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
>