diff mbox series

Fix issue with printvars executing giant shell command

Message ID 20180817231548.29867-1-tpiepho@impinj.com
State Superseded
Headers show
Series Fix issue with printvars executing giant shell command | expand

Commit Message

Trent Piepho Aug. 17, 2018, 11:15 p.m. UTC
The underlying problem is that $(foreach V,1 2 3,) does not evaluate to
an empty string.  It evaluates to "  ", three empty strings separated by
whitespace.

A construct of this format, with a giant list in the foreach, is part of
the printvars command.  This means that "@:$(foreach ....)", which is
intended to expand to a null command, in fact expands to "@:       "
with a great deal of whitespace.  Make chooses to execute this command
with:
    execve("/bin/sh", ["/bin/sh", "-c", ":       "]

But with far more whitespace.  So much that it can exceed shell command
line length limits.

The solution here is to use $(strip $(foreach ...)), so the command
expands to "@:", which make is smart enough to not even execute and
wouldn't exceed any limits if it did.

In some cases, make will not invoke a command via "sh -c" and will exec
it directly.  In this case make does command line argument parsing and
will swallow all the whitespace without overflowing a /bin/sh limit.  It
proved difficult to write a command that ensured make would do this.

Signed-off-by: Trent Piepho <tpiepho@impinj.com>
---
 Makefile | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

Comments

Yann E. MORIN Aug. 29, 2018, 8:49 p.m. UTC | #1
Trent, All,

On 2018-08-17 16:15 -0700, Trent Piepho spake thusly:
> The underlying problem is that $(foreach V,1 2 3,) does not evaluate to
> an empty string.  It evaluates to "  ", three empty strings separated by
> whitespace.
> 
> A construct of this format, with a giant list in the foreach, is part of
> the printvars command.  This means that "@:$(foreach ....)", which is
> intended to expand to a null command, in fact expands to "@:       "
> with a great deal of whitespace.  Make chooses to execute this command
> with:
>     execve("/bin/sh", ["/bin/sh", "-c", ":       "]
> 
> But with far more whitespace.  So much that it can exceed shell command
> line length limits.
> 
> The solution here is to use $(strip $(foreach ...)), so the command
> expands to "@:", which make is smart enough to not even execute and
> wouldn't exceed any limits if it did.
> 
> In some cases, make will not invoke a command via "sh -c" and will exec
> it directly.  In this case make does command line argument parsing and
> will swallow all the whitespace without overflowing a /bin/sh limit.  It
> proved difficult to write a command that ensured make would do this.
> 
> Signed-off-by: Trent Piepho <tpiepho@impinj.com>
> ---
>  Makefile | 4 ++--
>  1 file changed, 2 insertions(+), 2 deletions(-)
> 
> diff --git a/Makefile b/Makefile
> index 0115c4dfcc..b396c56042 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -988,13 +988,13 @@ $(BUILD_DIR)/.br2-external.in: $(BUILD_DIR)
>  # displayed.
>  .PHONY: printvars
>  printvars:
> -	@:$(foreach V, \
> +	@:$(strip $(foreach V, \

Why do we even need an actual command here? It works exactly the same
without a rule, and this indeed also gets rid of a call to a shell (and
consequently won't exceed the command line length limit).

I.e. the following:

    diff --git a/Makefile b/Makefile
    index 413ec921cd..b7b1257ea6 100644
    --- a/Makefile
    +++ b/Makefile
    @@ -977,7 +977,7 @@ $(BUILD_DIR)/.br2-external.in: $(BUILD_DIR)
     # displayed.
     .PHONY: printvars
     printvars:
    -	@:$(foreach V, \
    +	$(foreach V, \
     		$(sort $(if $(VARS),$(fil		ter $(VARS),$(.VARIABLES)),$(.VARIABLES))), \
     		$(if $(filter-out environment% default automatic, \
     				$(origin $V)), \

Do you see a case where that would not work?

FTR I tried to strace it to find the shells;

    $ strace -o m.log -ff -e trace=process -- make -s printvars >/dev/null
    $ grep execve m.log* |grep '                  '
    --> nothing

(before the patch above, there was one hit on the grep.)

Regards,
Yann E. MORIN.

>  		$(sort $(if $(VARS),$(filter $(VARS),$(.VARIABLES)),$(.VARIABLES))), \
>  		$(if $(filter-out environment% default automatic, \
>  				$(origin $V)), \
>  		$(if $(QUOTED_VARS),\
>  			$(info $V='$(subst ','\'',$(if $(RAW_VARS),$(value $V),$($V)))'), \
> -			$(info $V=$(if $(RAW_VARS),$(value $V),$($V))))))
> +			$(info $V=$(if $(RAW_VARS),$(value $V),$($V)))))))
>  # ' Syntax colouring...
>  
>  .PHONY: clean
> -- 
> 2.14.4
> 
> _______________________________________________
> buildroot mailing list
> buildroot@busybox.net
> http://lists.busybox.net/mailman/listinfo/buildroot
Trent Piepho Sept. 10, 2018, 5:45 p.m. UTC | #2
On Wed, 2018-08-29 at 22:49 +0200, Yann E. MORIN wrote:
> On 2018-08-17 16:15 -0700, Trent Piepho spake thusly:
> > 
> > The solution here is to use $(strip $(foreach ...)), so the command
> > expands to "@:", which make is smart enough to not even execute and
> > wouldn't exceed any limits if it did.

> > --- a/Makefile
> > +++ b/Makefile
> > @@ -988,13 +988,13 @@ $(BUILD_DIR)/.br2-external.in: $(BUILD_DIR)
> >  # displayed.
> >  .PHONY: printvars
> >  printvars:
> > -	@:$(foreach V, \
> > +	@:$(strip $(foreach V, \
> 
> Why do we even need an actual command here? It works exactly the same
> without a rule, and this indeed also gets rid of a call to a shell (and
> consequently won't exceed the command line length limit).

Not quite exactly the same.  There's no longer a recipe at all, only
the definition of a dependency for printvars, so the output will
include:

make[1]: Nothing to be done for 'printvars'.

Which isn't expected output for something that was parsing printvars.

I'll also mention that we saw this error running buildroot in certain
docker containers, but not everywhere.  Whether or not make will pass a
giant sequence of whitespace to a shell ends up not being so simple,
and I couldn't find any precise rules that govern it.  So just because
it works as desired in one test doesn't mean is must work that way
everywhere.

However, it is clearly defined that strip will convert a sequence of
whitespace to an empty string, so I figured better to relay on that.
Yann E. MORIN Sept. 10, 2018, 6:18 p.m. UTC | #3
Trent, All,

On 2018-09-10 17:45 +0000, Trent Piepho spake thusly:
> On Wed, 2018-08-29 at 22:49 +0200, Yann E. MORIN wrote:
> > On 2018-08-17 16:15 -0700, Trent Piepho spake thusly:
> > > 
> > > The solution here is to use $(strip $(foreach ...)), so the command
> > > expands to "@:", which make is smart enough to not even execute and
> > > wouldn't exceed any limits if it did.
> 
> > > --- a/Makefile
> > > +++ b/Makefile
> > > @@ -988,13 +988,13 @@ $(BUILD_DIR)/.br2-external.in: $(BUILD_DIR)
> > >  # displayed.
> > >  .PHONY: printvars
> > >  printvars:
> > > -	@:$(foreach V, \
> > > +	@:$(strip $(foreach V, \
> > 
> > Why do we even need an actual command here? It works exactly the same
> > without a rule, and this indeed also gets rid of a call to a shell (and
> > consequently won't exceed the command line length limit).
> 
> Not quite exactly the same.  There's no longer a recipe at all, only
> the definition of a dependency for printvars, so the output will
> include:
> 
> make[1]: Nothing to be done for 'printvars'.

So what about:

diff --git a/Makefile b/Makefile
index 2c6af12989..57e776056c 100644
--- a/Makefile
+++ b/Makefile
@@ -988,7 +988,8 @@ $(BUILD_DIR)/.br2-external.in: $(BUILD_DIR)
 # displayed.
 .PHONY: printvars
 printvars:
-	@:$(foreach V, \
+	@:
+	$(foreach V, \
 		$(sort $(if $(VARS),$(filter $(VARS),$(.VARIABLES)),$(.VARIABLES))), \
 		$(if $(filter-out environment% default automatic, \
 				$(origin $V)), \

> Which isn't expected output for something that was parsing printvars.

Which should probably have already called it with 'make -s' because
that is what is also needed when printvars is called from an
out-of-tree build, by the way.

> I'll also mention that we saw this error running buildroot in certain
> docker containers, but not everywhere.  Whether or not make will pass a
> giant sequence of whitespace to a shell ends up not being so simple,
> and I couldn't find any precise rules that govern it.  So just because
> it works as desired in one test doesn't mean is must work that way
> everywhere.

Before my suggested hange:

    $ strace -ff -s 1048576 -o n.log -e trace=process -- make printvars >/dev/null
    $ grep execve n.log* |grep '                  '
    --> hit!

With the above change:

    $ strace -ff -s 1048576 -o n.log -e trace=process -- make printvars >/dev/null
    $ grep execve n.log* |grep '                  '
    --> no hit!

In a recipe, a command line that contains only spaces will not be executed
by make.

So with the above, you get the silence from a rule, and no long-shell
commands that break.

> However, it is clearly defined that strip will convert a sequence of
> whitespace to an empty string, so I figured better to relay on that.

I'm just trying to keep it simple.

Regards,
Yann E. MORIN.
Trent Piepho Sept. 17, 2018, 6:02 p.m. UTC | #4
On Mon, 2018-09-10 at 20:18 +0200, Yann E. MORIN wrote:
> On 2018-09-10 17:45 +0000, Trent Piepho spake thusly:
> > On Wed, 2018-08-29 at 22:49 +0200, Yann E. MORIN wrote:
> > > On 2018-08-17 16:15 -0700, Trent Piepho spake thusly:
> > > > 
> > > > The solution here is to use $(strip $(foreach ...)), so the command
> > > > expands to "@:", which make is smart enough to not even execute and
> > > > wouldn't exceed any limits if it did.
> > > > 
> So what about:
> 
> diff --git a/Makefile b/Makefile
> index 2c6af12989..57e776056c 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -988,7 +988,8 @@ $(BUILD_DIR)/.br2-external.in: $(BUILD_DIR)
>  # displayed.
>  .PHONY: printvars
>  printvars:
> -	@:$(foreach V, \
> +	@:
> +	$(foreach V, \
>  		$(sort $(if $(VARS),$(filter $(VARS),$(.VARIABLES)),$(.VARIABLES))), \
>  		$(if $(filter-out environment% default automatic, \
>  				$(origin $V)), \
> 
> In a recipe, a command line that contains only spaces will not be executed
> by make.
> 
> So with the above, you get the silence from a rule, and no long-shell
> commands that break.
> 
> > However, it is clearly defined that strip will convert a sequence of
> > whitespace to an empty string, so I figured better to relay on that.
> 
> I'm just trying to keep it simple.

Ok, if you like that better.  My thought was "strip eats whitespace" is
simple, but "a recipe line starting with a tab and containing only
whitespace is skipped" is more obscure.  In fact, while it does appear
to be true, I haven't found anything in the make docs clearly stating
this.
Yann E. MORIN Sept. 17, 2018, 7:54 p.m. UTC | #5
Trent, All,

On 2018-09-17 18:02 +0000, Trent Piepho spake thusly:
> On Mon, 2018-09-10 at 20:18 +0200, Yann E. MORIN wrote:
> > In a recipe, a command line that contains only spaces will not be executed
> > by make.
[--SNIP--]
> Ok, if you like that better.  My thought was "strip eats whitespace" is
> simple, but "a recipe line starting with a tab and containing only
> whitespace is skipped" is more obscure.  In fact, while it does appear
> to be true, I haven't found anything in the make docs clearly stating
> this.

From the make manual, chapter 5.1, "Recipe Syntax" [0], states:

    * A blank line that begins with a tab is not blank: it’s an empty
      recipe (see Empty Recipes).

... where "Empty Recipes" is a link to chapter 5.9, titled "Using
Empty Recipes" [1], which starts with:

    It is sometimes useful to define recipes which do nothing. This
    is done simply by giving a recipe that consists of nothing but
    whitespace.

[0] https://www.gnu.org/software/make/manual/make.html#Recipe-Syntax
[1] https://www.gnu.org/software/make/manual/make.html#Empty-Recipes

Regards,
Yann E. MORIN.
diff mbox series

Patch

diff --git a/Makefile b/Makefile
index 0115c4dfcc..b396c56042 100644
--- a/Makefile
+++ b/Makefile
@@ -988,13 +988,13 @@  $(BUILD_DIR)/.br2-external.in: $(BUILD_DIR)
 # displayed.
 .PHONY: printvars
 printvars:
-	@:$(foreach V, \
+	@:$(strip $(foreach V, \
 		$(sort $(if $(VARS),$(filter $(VARS),$(.VARIABLES)),$(.VARIABLES))), \
 		$(if $(filter-out environment% default automatic, \
 				$(origin $V)), \
 		$(if $(QUOTED_VARS),\
 			$(info $V='$(subst ','\'',$(if $(RAW_VARS),$(value $V),$($V)))'), \
-			$(info $V=$(if $(RAW_VARS),$(value $V),$($V))))))
+			$(info $V=$(if $(RAW_VARS),$(value $V),$($V)))))))
 # ' Syntax colouring...
 
 .PHONY: clean