https://gcc.gnu.org/g:619a5e5586759a3ed16baf1333728664b06b0451
commit r17-1526-g619a5e5586759a3ed16baf1333728664b06b0451 Author: Sunil Dora <[email protected]> Date: Thu Jun 11 18:20:20 2026 +0530 driver: Spill long COLLECT_GCC_OPTIONS to a response file [PR111527] 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 assembled value exceeds such a limit, the build fails. When the assembled value would exceed COLLECT2_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 (COLLECT2_OPTIONS_MAX_LENGTH): New macro. * collect-utils.cc (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/hostconfig.texi (Host Misc): Document COLLECT2_OPTIONS_MAX_LENGTH. * 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. include/ChangeLog: * libiberty.h (expandargstr): Declare. libiberty/ChangeLog: * argv.c (expandargstr): New function. lto-plugin/ChangeLog: * lto-plugin.c (read_collect_gcc_options): New function. (onload): Use read_collect_gcc_options instead of getenv. Signed-off-by: Sunil Dora <[email protected]> Diff: --- gcc/collect-utils.cc | 19 ++++++++ gcc/collect-utils.h | 2 +- gcc/collect2.cc | 6 +-- gcc/defaults.h | 6 +++ gcc/doc/hostconfig.texi | 5 ++ gcc/doc/invoke.texi | 7 +++ gcc/gcc.cc | 48 ++++++++++++++++++- gcc/lto-wrapper.cc | 2 +- gcc/testsuite/gcc.misc-tests/pr111527.exp | 78 +++++++++++++++++++++++++++++++ include/libiberty.h | 5 ++ libiberty/argv.c | 75 +++++++++++++++++++++++++++++ lto-plugin/lto-plugin.c | 22 ++++++++- 12 files changed, 268 insertions(+), 7 deletions(-) diff --git a/gcc/collect-utils.cc b/gcc/collect-utils.cc index ad37e7a49057..0af00a7009d0 100644 --- a/gcc/collect-utils.cc +++ b/gcc/collect-utils.cc @@ -269,3 +269,22 @@ utils_cleanup (bool from_signal) tool_cleanup (from_signal); } + +/* 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; + + if (cached) + return cached; + + const char *raw = getenv ("COLLECT_GCC_OPTIONS"); + if (raw == nullptr) + return nullptr; + + cached = expandargstr (tool_name, raw); + return cached; +} diff --git a/gcc/collect-utils.h b/gcc/collect-utils.h index 3ed80271fdb5..50b4bba9a0b5 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 6985e0b4d159..c7b0ad4321ac 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 87f710697f0c..cbb4d1bbb924 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 COLLECT2_OPTIONS_MAX_LENGTH +#define COLLECT2_OPTIONS_MAX_LENGTH 1024 +#endif + #endif /* ! GCC_DEFAULTS_H */ diff --git a/gcc/doc/hostconfig.texi b/gcc/doc/hostconfig.texi index fc163c163c21..9682985124e9 100644 --- a/gcc/doc/hostconfig.texi +++ b/gcc/doc/hostconfig.texi @@ -205,6 +205,11 @@ will not work reliably. If defined, a C statement (sans semicolon) that performs host-dependent initialization when @code{collect2} is being initialized. +@item COLLECT2_OPTIONS_MAX_LENGTH +If defined, the maximum byte length of @env{COLLECT_GCC_OPTIONS} +before the driver spills it to a response file. The default is +@code{1024}. + @item GCC_DRIVER_HOST_INITIALIZATION If defined, a C statement (sans semicolon) that performs host-dependent initialization when a compilation driver is being initialized. diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi index 17e10b4b1b42..24d0420ca381 100644 --- a/gcc/doc/invoke.texi +++ b/gcc/doc/invoke.texi @@ -37776,6 +37776,13 @@ 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 is too long (over 1 KiB by default), 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 0ed2cd96be15..b0e68fb645f7 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 COLLECT2_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) <= COLLECT2_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 9610fa5b7a82..ca39f51e1863 100644 --- a/gcc/lto-wrapper.cc +++ b/gcc/lto-wrapper.cc @@ -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 = const_cast<char *> (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 000000000000..0e71519483f3 --- /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 +# COLLECT2_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/include/libiberty.h b/include/libiberty.h index ae0e94bc4c5f..e905f2cf1902 100644 --- a/include/libiberty.h +++ b/include/libiberty.h @@ -94,6 +94,11 @@ extern int writeargv (char * const *, FILE *); extern int countargv (char * const *); +/* Expand VAL as a response file if it begins with '@' and return the + result as a shell-quoted string. */ + +extern char *expandargstr (const char *, const char *); + /* Return the last component of a path name. Note that we can't use a prototype here because the parameter is declared inconsistently across different systems, sometimes as "char *" and sometimes as diff --git a/libiberty/argv.c b/libiberty/argv.c index 64127f70c992..35e2750c5ff3 100644 --- a/libiberty/argv.c +++ b/libiberty/argv.c @@ -492,6 +492,81 @@ countargv (char * const *argv) return argc; } +/* + +@deftypefn Extension {char *} expandargstr @ + (const char *@var{progname}, const char *@var{val}) + +Expand @var{val} as a response file via @code{expandargv} if it begins +with @samp{@@}, using @var{progname} as @code{argv[0]}, and return the +expanded option list as a shell-quoted string. Returns a newly +allocated string. + +@end deftypefn + +*/ + +char * +expandargstr (const char *progname, const char *val) +{ + int argc = 2; + char **argv; + char **orig; + size_t len; + char *buf; + char *p; + int i; + + if (val[0] != '@') + return xstrdup (val); + + argv = (char **) xcalloc (3, sizeof (char *)); + orig = argv; + argv[0] = xstrdup (progname); + argv[1] = xstrdup (val); + argv[2] = NULL; + expandargv (&argc, &argv); + if (argv != orig) + freeargv (orig); + + len = 1; + for (i = 1; argv[i] != NULL; i++) + { + const char *q; + if (i > 1) + len++; + len += 2; + for (q = argv[i]; *q; q++) + len += (*q == '\'') ? 4 : 1; + } + + buf = (char *) xmalloc (len); + p = buf; + for (i = 1; argv[i] != NULL; i++) + { + const char *q; + if (i > 1) + *p++ = ' '; + *p++ = '\''; + for (q = argv[i]; *q; q++) + { + if (*q == '\'') + { + *p++ = '\''; + *p++ = '\\'; + *p++ = '\''; + *p++ = '\''; + } + else + *p++ = *q; + } + *p++ = '\''; + } + *p = '\0'; + freeargv (argv); + return buf; +} + #ifdef MAIN /* Simple little test driver. */ diff --git a/lto-plugin/lto-plugin.c b/lto-plugin/lto-plugin.c index ffa4fe1552fd..c4d09f3b7a48 100644 --- a/lto-plugin/lto-plugin.c +++ b/lto-plugin/lto-plugin.c @@ -1506,6 +1506,26 @@ negotiate_api_version (void) } } +/* Return COLLECT_GCC_OPTIONS, expanding an @file reference if present. + Returns NULL if unset. Result owned by an internal cache. */ + +static const char * +read_collect_gcc_options (void) +{ + static char *cached; + const char *raw; + + if (cached) + return cached; + + raw = getenv ("COLLECT_GCC_OPTIONS"); + if (raw == NULL) + return NULL; + + cached = expandargstr ("lto-plugin", raw); + return cached; +} + /* Called by a linker after loading the plugin. TV is the transfer vector. */ enum ld_plugin_status @@ -1617,7 +1637,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
