retitle 782672 [PATCH] sbuild: Extra command parsing is overly-complex and 
broken
tags 782672 patch
thanks

I made a patch to address this. It's attached. The patch says that it's
2/3 in a series, but this is the only one relevant to this issue.

Notes:

when handling external commands (--starting-build-commands and others),
I now keep the input string as is, and pass it to the shell, instead of
splitting it on whitespace and then reassembling it. This reassembling
was being done improperly, with shell internals such as < being
erroneously escaped (doing it right is way more work than simply keeping
the original string). Keeping the string as is allows the user to give
the command they want.

This is implemented by keeping the COMMAND and INTCOMMAND keys same as
they were (array-refs of strings), but adds additional COMMAND_STR and
INTCOMMAND_STR keys that have the string representation. The non-STR
versions are always populated, and the external command options populate
the _STR ones as well. When running the commands, we use the _STR
options if we have them, otherwise, we recombine as before.

Since the old array-ref-based code path is still there, old commandlines
and configurations remain working.

The big win so far is that I can get an interactive shell going. It's
not ideal yet since I have to manually redirect the pty, and there's
some stdout buffering thing I haven't tried to fix, but it's already
super useful:

    $ ls -l /dev/stdin(:A)
    crw------- 1 dkogan tty 136, 1 Apr 21 12:42 /dev/pts/1

    $ sbuild .... --starting-build-commands 'sh -i < /dev/pts/1 >
    /dev/pts/1'

    
+------------------------------------------------------------------------------+
    | Starting Timed Build Commands                                             
   |
    
+------------------------------------------------------------------------------+


    sh -i < /dev/pts/1 > /dev/pts/1
    -------------------------------

    pwd
    /build/cross-gcc-4.9-i386-uMV_ms



>From 9eeea9238147bce12e5544751b6e4106df24571c Mon Sep 17 00:00:00 2001
From: Dima Kogan <d...@secretsauce.net>
Date: Mon, 20 Apr 2015 22:12:56 -0700
Subject: [PATCH 2/3] external commands are now unparsed strings

when handling external commands (--starting-build-commands and others), I now
keep the input string as is, and pass it to the shell, instead of splitting it
on whitespace and then reassembling it. This reassembling was being done
improperly, with shell internals such as < being erroneously escaped. Keeping
the string as is allows the user to give the command they want.

This is implemented by keeping the COMMAND and INTCOMMAND keys same as they were
(array-refs of strings), but adds additional COMMAND_STR and INTCOMMAND_STR keys
that have the string representation. The non-STR versions are always populated,
and the external command options populate the _STR ones as well. When running
the commands, we use the _STR options if we have them, otherwise, we recombine
as before.

Since the old array-ref-based code path is still there, old commandlines and
configurations remain working
---
 lib/Sbuild/Build.pm         | 71 +++++++++++++++++++++++++++++++--------------
 lib/Sbuild/Chroot.pm        |  4 +++
 lib/Sbuild/ChrootPlain.pm   | 41 +++++++++++++++-----------
 lib/Sbuild/ChrootSchroot.pm | 14 ++++++---
 lib/Sbuild/ChrootSudo.pm    | 43 +++++++++++++++------------
 lib/Sbuild/Conf.pm          |  2 +-
 lib/Sbuild/Options.pm       | 21 +++++---------
 7 files changed, 119 insertions(+), 77 deletions(-)

diff --git a/lib/Sbuild/Build.pm b/lib/Sbuild/Build.pm
index b44ccb3..03e5522 100644
--- a/lib/Sbuild/Build.pm
+++ b/lib/Sbuild/Build.pm
@@ -1081,23 +1081,37 @@ sub run_command {
 	    $defaults = $self->get('Host')->{'Defaults'};
 	    $out = $defaults->{'STREAMOUT'} if ($log_output);
 	    $err = $defaults->{'STREAMERR'} if ($log_error);
-	    $self->get('Host')->run_command(
-		{ COMMAND => \@{$command},
-		    PRIORITY => 0,
-		    STREAMOUT => $out,
-		    STREAMERR => $err,
-		});
+
+	    my %args = (PRIORITY  => 0,
+			STREAMOUT => $out,
+			STREAMERR => $err);
+	    if(ref $command) {
+		$args{COMMAND} = \@{$command};
+		$args{COMMAND_STR} = "@{$command}";
+	    } else {
+		$args{COMMAND} = [split('\s+', $command)];
+		$args{COMMAND_STR} = $command;
+	    }
+
+	    $self->get('Host')->run_command( \%args );
 	} else {
 	    $defaults = $self->get('Session')->{'Defaults'};
 	    $out = $defaults->{'STREAMOUT'} if ($log_output);
 	    $err = $defaults->{'STREAMERR'} if ($log_error);
-	    $self->get('Session')->run_command(
-		{ COMMAND => \@{$command},
-		    USER => ($rootuser ? 'root' : $self->get_conf('BUILD_USER')),
-		    PRIORITY => 0,
-		    STREAMOUT => $out,
-		    STREAMERR => $err,
-		});
+
+	    my %args = (USER => ($rootuser ? 'root' : $self->get_conf('BUILD_USER')),
+			PRIORITY => 0,
+			STREAMOUT => $out,
+			STREAMERR => $err);
+	    if(ref $command) {
+		$args{COMMAND} = \@{$command};
+		$args{COMMAND_STR} = "@{$command}";
+	    } else {
+		$args{COMMAND} = [split('\s+', $command)];
+		$args{COMMAND_STR} = $command;
+	    }
+
+	    $self->get('Session')->run_command( \%args );
 	}
     my $status = $?;
 
@@ -1172,17 +1186,30 @@ sub run_external_commands {
 	sort {length $b <=> length $a || $a cmp $b} keys %percent);
     my $returnval = 1;
     foreach my $command (@commands) {
-	foreach my $arg (@{$command}) {
-	    $arg =~ s{
-	        # Match a percent followed by a valid keyword
-	       \%($keyword_pat)
-	    }{
-	        # Substitute with the appropriate value only if it's defined
-	        $percent{$1} || $&
-	    }msxge;
+
+	my $substitute = sub {
+	    foreach(@_) {
+		s{
+		     # Match a percent followed by a valid keyword
+		     \%($keyword_pat)
+	     }{
+		 # Substitute with the appropriate value only if it's defined
+		 $percent{$1} || $&
+	     }msxge;
+	    }
+	};
+
+	my $command_str;
+	if( ref $command ) {
+	    $substitute->(@{$command});
+	    $command_str = join(" ", @{$command});
+	} else {
+	    $substitute->($command);
+	    $command_str = $command;
 	}
-	my $command_str = join(" ", @{$command});
+
 	$self->log_subsubsection("$command_str");
+
 	$returnval = $self->run_command($command, $log_output, $log_error, $chroot, $rootuser);
 	$self->log("\n");
 	if (!$returnval) {
diff --git a/lib/Sbuild/Chroot.pm b/lib/Sbuild/Chroot.pm
index ad193c8..366fb1e 100644
--- a/lib/Sbuild/Chroot.pm
+++ b/lib/Sbuild/Chroot.pm
@@ -242,6 +242,7 @@ sub run_command {
     my $options = shift;
 
     $options->{'INTCOMMAND'} = copy($options->{'COMMAND'});
+    $options->{'INTCOMMAND_STR'} = copy($options->{'COMMAND_STR'});
 
     return $self->run_command_internal($options);
 }
@@ -254,6 +255,7 @@ sub pipe_command {
     my $options = shift;
 
     $options->{'INTCOMMAND'} = copy($options->{'COMMAND'});
+    $options->{'INTCOMMAND_STR'} = copy($options->{'COMMAND_STR'});
 
     return $self->pipe_command_internal($options);
 }
@@ -306,7 +308,9 @@ sub exec_command {
 
     debug2("PROGRAM: $program\n");
     debug2("COMMAND: ", join(" ", @{$options->{'COMMAND'}}), "\n");
+    debug2("COMMAND_STR: ", $options->{'COMMAND'} // 'UNDEFINED', "\n");
     debug2("INTCOMMAND: ", join(" ", @{$options->{'INTCOMMAND'}}), "\n");
+    debug2("INTCOMMAND_STR: ", $options->{'INTCOMMAND_STR:'} // 'UNDEFINED', "\n");
     debug2("EXPCOMMAND: ", join(" ", @{$options->{'EXPCOMMAND'}}), "\n");
 
     debug2("Environment set:\n");
diff --git a/lib/Sbuild/ChrootPlain.pm b/lib/Sbuild/ChrootPlain.pm
index 477bb7a..e8c8ee6 100644
--- a/lib/Sbuild/ChrootPlain.pm
+++ b/lib/Sbuild/ChrootPlain.pm
@@ -80,7 +80,9 @@ sub get_command_internal {
     my $self = shift;
     my $options = shift;
 
-    my $command = $options->{'INTCOMMAND'}; # Command to run
+    # Command to run. If I have a string, use it. Otherwise use the list-ref
+    my $command = $options->{'INTCOMMAND_STR'} // $options->{'INTCOMMAND'};
+
     my $user = $options->{'USER'};          # User to run command under
     my $dir;                                # Directory to use (optional)
     $dir = $self->get('Defaults')->{'DIR'} if
@@ -99,20 +101,6 @@ sub get_command_internal {
 	$dir = '/';
     }
 
-    my $shellcommand;
-    foreach (@$command) {
-	my $tmp = $_;
-	$tmp =~ s/'//g; # Strip any single quotes for security
-	if ($_ ne $tmp) {
-	    $self->log_warning("Stripped single quote from command for security: $_\n");
-	}
-	if ($shellcommand) {
-	    $shellcommand .= " '$tmp'";
-	} else {
-	    $shellcommand = "'$tmp'";
-	}
-    }
-
     my $need_chroot = 0;
     $need_chroot = 1
 	if ($self->get('Location') ne '/');
@@ -128,10 +116,29 @@ sub get_command_internal {
 	if ($need_chroot);
     push(@cmdline, $self->get_conf('SU'), "$user", '-s')
 	if ($need_su);
-    push(@cmdline, '/bin/sh', '-c', "cd '$dir' && $shellcommand");
+
+
+    if( ref $command ) {
+        my $shellcommand;
+        foreach (@$command) {
+            my $tmp = $_;
+            $tmp =~ s/'//g; # Strip any single quotes for security
+            if ($_ ne $tmp) {
+                $self->log_warning("Stripped single quote from command for security: $_\n");
+            }
+            if ($shellcommand) {
+                $shellcommand .= " '$tmp'";
+            } else {
+                $shellcommand = "'$tmp'";
+            }
+        }
+        push(@cmdline, '/bin/sh', '-c', "cd '$dir' && $shellcommand");
+    } else {
+        push(@cmdline, '/bin/sh', '-c', "cd '$dir' && ( $command )");
+    }
 
     $options->{'USER'} = $user;
-    $options->{'COMMAND'} = $command;
+    $options->{'COMMAND'} = ref($command) ? $command : [split(/\s+/, $command)];
     $options->{'EXPCOMMAND'} = \@cmdline;
     $options->{'CHDIR'} = undef;
     $options->{'DIR'} = $dir;
diff --git a/lib/Sbuild/ChrootSchroot.pm b/lib/Sbuild/ChrootSchroot.pm
index 3a4ab13..b11cabc 100644
--- a/lib/Sbuild/ChrootSchroot.pm
+++ b/lib/Sbuild/ChrootSchroot.pm
@@ -102,7 +102,9 @@ sub get_command_internal {
     my $self = shift;
     my $options = shift;
 
-    my $command = $options->{'INTCOMMAND'}; # Command to run
+    # Command to run. If I have a string, use it. Otherwise use the list-ref
+    my $command = $options->{'INTCOMMAND_STR'} // $options->{'INTCOMMAND'};
+
     my $user = $options->{'USER'};          # User to run command under
     my $dir;                                # Directory to use (optional)
     $dir = $self->get('Defaults')->{'DIR'} if
@@ -125,9 +127,13 @@ sub get_command_internal {
 		'-c', $self->get('Session ID'),
 		'--run-session',
 		@{$self->get_conf('SCHROOT_OPTIONS')},
-		'-u', "$user", '-p', '--',
-		@$command);
-
+		'-u', "$user", '-p', '--');
+    if (ref $command) {
+        push @cmdline, @$command;
+    } else {
+        push @cmdline, ('/bin/sh', '-c', $command);
+        $command = [split(/\s+/, $command)];
+    }
     $options->{'USER'} = $user;
     $options->{'COMMAND'} = $command;
     $options->{'EXPCOMMAND'} = \@cmdline;
diff --git a/lib/Sbuild/ChrootSudo.pm b/lib/Sbuild/ChrootSudo.pm
index b7cc623..b36642c 100644
--- a/lib/Sbuild/ChrootSudo.pm
+++ b/lib/Sbuild/ChrootSudo.pm
@@ -83,7 +83,9 @@ sub get_command_internal {
     my $self = shift;
     my $options = shift;
 
-    my $command = $options->{'INTCOMMAND'}; # Command to run
+    # Command to run. If I have a string, use it. Otherwise use the list-ref
+    my $command = $options->{'INTCOMMAND_STR'} // $options->{'INTCOMMAND'};
+
     my $user = $options->{'USER'};          # User to run command under
     my $dir;                                # Directory to use (optional)
     $dir = $self->get('Defaults')->{'DIR'} if
@@ -102,27 +104,30 @@ sub get_command_internal {
 	$dir = '/';
     }
 
-    my $shellcommand;
-    foreach (@$command) {
-	my $tmp = $_;
-	$tmp =~ s/'//g; # Strip any single quotes for security
-	if ($_ ne $tmp) {
-	    $self->log_warning("Stripped single quote from command for security: $_\n");
-	}
-	if ($shellcommand) {
-	    $shellcommand .= " '$tmp'";
-	} else {
-	    $shellcommand = "'$tmp'";
-	}
-    }
-
     @cmdline = ($self->get_conf('SUDO'), '/usr/sbin/chroot', $self->get('Location'),
-		$self->get_conf('SU'), "$user", '-s',
-		'/bin/sh', '-c',
-		"cd '$dir' && $shellcommand");
+		$self->get_conf('SU'), "$user", '-s');
+
+    if( ref $command ) {
+        my $shellcommand;
+        foreach (@$command) {
+            my $tmp = $_;
+            $tmp =~ s/'//g; # Strip any single quotes for security
+            if ($_ ne $tmp) {
+                $self->log_warning("Stripped single quote from command for security: $_\n");
+            }
+            if ($shellcommand) {
+                $shellcommand .= " '$tmp'";
+            } else {
+                $shellcommand = "'$tmp'";
+            }
+        }
+        push(@cmdline, '/bin/sh', '-c', "cd '$dir' && $shellcommand");
+    } else {
+        push(@cmdline, '/bin/sh', '-c', "cd '$dir' && ( $command )");
+    }
 
     $options->{'USER'} = $user;
-    $options->{'COMMAND'} = $command;
+    $options->{'COMMAND'} = ref($command) ? $command : [split(/\s+/, $command)];
     $options->{'EXPCOMMAND'} = \@cmdline;
     $options->{'CHDIR'} = undef;
     $options->{'DIR'} = $dir;
diff --git a/lib/Sbuild/Conf.pm b/lib/Sbuild/Conf.pm
index 6ccbba5..d8e5f0c 100644
--- a/lib/Sbuild/Conf.pm
+++ b/lib/Sbuild/Conf.pm
@@ -965,7 +965,7 @@ sub setup ($) {
 	    HELP => 'Preceding arguments to launch piuparts as root. If no arguments are specified, piuparts will be launched via sudo.'
 	},
 	'EXTERNAL_COMMANDS'			=> {
-	    TYPE => 'HASH:ARRAY:ARRAY:STRING',
+	    TYPE => 'HASH:ARRAY:STRING',
 	    VARNAME => 'external_commands',
 	    GROUP => 'Chroot options',
 	    DEFAULT => {
diff --git a/lib/Sbuild/Options.pm b/lib/Sbuild/Options.pm
index d978f71..10e02f2 100644
--- a/lib/Sbuild/Options.pm
+++ b/lib/Sbuild/Options.pm
@@ -204,9 +204,8 @@ sub set_options {
 			   $self->set_conf('STATS_DIR', $_[1]);
 		       },
 		       "setup-hook=s" => sub {
-			my @command = split(/\s+/, $_[1]);
 			push(@{${$self->get_conf('EXTERNAL_COMMANDS')}{"chroot-setup-commands"}},
-			\@command);
+			$_[1]);
 			   $self->set_conf('CHROOT_SETUP_SCRIPT', $_[1]);
 		       },
 		       "use-snapshot" => sub {
@@ -268,34 +267,28 @@ sub set_options {
 				$_[1]);
 		       },
 			"pre-build-commands=s" => sub {
-			   my @command = split(/\s+/, $_[1]);
 			   push(@{${$self->get_conf('EXTERNAL_COMMANDS')}{"pre-build-commands"}},
-				\@command);
+				$_[1]);
 		       },
 			"chroot-setup-commands=s" => sub {
-			   my @command = split(/\s+/, $_[1]);
 			   push(@{${$self->get_conf('EXTERNAL_COMMANDS')}{"chroot-setup-commands"}},
-				\@command);
+				$_[1]);
 		       },
 			"starting-build-commands=s" => sub {
-			   my @command = split(/\s+/, $_[1]);
 			   push(@{${$self->get_conf('EXTERNAL_COMMANDS')}{"starting-build-commands"}},
-				\@command);
+				$_[1]);
 		       },
 			"finished-build-commands=s" => sub {
-			   my @command = split(/\s+/, $_[1]);
 			   push(@{${$self->get_conf('EXTERNAL_COMMANDS')}{"finished-build-commands"}},
-				\@command);
+				$_[1]);
 		       },
 			"chroot-cleanup-commands=s" => sub {
-			   my @command = split(/\s+/, $_[1]);
 			   push(@{${$self->get_conf('EXTERNAL_COMMANDS')}{"chroot-cleanup-commands"}},
-				\@command);
+				$_[1]);
 		       },
 			"post-build-commands=s" => sub {
-			   my @command = split(/\s+/, $_[1]);
 			   push(@{${$self->get_conf('EXTERNAL_COMMANDS')}{"post-build-commands"}},
-				\@command);
+				$_[1]);
 		       },
 			"log-external-command-output" => sub {
 			    $self->set_conf('LOG_EXTERNAL_COMMAND_OUTPUT', 1);
-- 
2.1.4

Reply via email to