On Tue, Jun 9, 2026 at 12:28 AM Shivam Gupta <[email protected]> wrote:
>
>
>
> 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?
Maybe it can be placed in libiberty/argv.c and then passed the getenv.
That would work. Since that is used by both lto-plugin and gcc already.
>
> 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
>>