parallel: Use a perl script wrapper to avoid security issue with race condition:

Attacker symlinks a file that will be created later.
This commit is contained in:
Ole Tange 2015-04-17 23:51:45 +02:00
parent 1d1d35cfdd
commit b9be3f78ba
6 changed files with 64 additions and 46 deletions

View file

@ -215,10 +215,19 @@ GNU Parallel 20150422 ('Germanwings') has been released. It is available for dow
Haiku of the month: Haiku of the month:
<<>> SSH set up?
Instant cluster needed now?
Use GNU Parallel.
-- Ole Tange
New in this release: New in this release:
* Security fix. An attacker on the local system could make you overwrite one of your own files with a single byte. The problem requires:
- you using --compress or --tmux
- the attacker must figure out the randomly chosen file name
- the attacker to create a symlink within a time window of 15 ms
* GNU Parallel now has a DOI: https://dx.doi.org/10.5281/zenodo.16303 * GNU Parallel now has a DOI: https://dx.doi.org/10.5281/zenodo.16303
* GNU Parallel was cited in: Scaling Machine Learning for Target Prediction in Drug Discovery using Apache Spark https://cris.cumulus.vub.ac.be/portal/files/5147244/spark.pdf * GNU Parallel was cited in: Scaling Machine Learning for Target Prediction in Drug Discovery using Apache Spark https://cris.cumulus.vub.ac.be/portal/files/5147244/spark.pdf

View file

@ -5579,23 +5579,22 @@ sub slot {
# cat followed by tail (possibly with rm as soon at the file is opened) # cat followed by tail (possibly with rm as soon at the file is opened)
# If $writerpid dead: finish after this round # If $writerpid dead: finish after this round
use Fcntl; use Fcntl;
$|=1; $|=1;
my ($cmd, $writerpid, $read_file, $unlink_file) = @ARGV; my ($comfile, $cmd, $writerpid, $read_file, $unlink_file) = @ARGV;
while(defined $unlink_file and not -e $unlink_file) {
# Writer has not opened file, so we cannot open it
$sleep = ($sleep < 30) ? ($sleep * 1.001 + 0.01) : ($sleep);
usleep($sleep);
}
if($read_file) { if($read_file) {
open(IN,"<",$read_file) || die("cattail: Cannot open $read_file"); open(IN,"<",$read_file) || die("cattail: Cannot open $read_file");
} else { } else {
*IN = *STDIN; *IN = *STDIN;
} }
while(! -s $comfile) {
# Writer has not opened the buffer file, so we cannot remove it yet
$sleep = ($sleep < 30) ? ($sleep * 1.001 + 0.01) : ($sleep);
usleep($sleep);
}
# The writer and we have both opened the file, so it is safe to unlink it # The writer and we have both opened the file, so it is safe to unlink it
unlink $unlink_file; unlink $unlink_file;
unlink $comfile;
my $first_round = 1; my $first_round = 1;
my $flags; my $flags;
@ -5630,8 +5629,8 @@ sub slot {
} }
# TODO This could probably be done more efficiently using select(2) # TODO This could probably be done more efficiently using select(2)
# Nothing read: Wait longer before next read # Nothing read: Wait longer before next read
# Up to 30 milliseconds # Up to 100 milliseconds
$sleep = ($sleep < 30) ? ($sleep * 1.001 + 0.01) : ($sleep); $sleep = ($sleep < 100) ? ($sleep * 1.001 + 0.01) : ($sleep);
usleep($sleep); usleep($sleep);
} }
} }
@ -5741,27 +5740,29 @@ sub grouped {
} }
} }
sub empty_input_detector { sub empty_input_wrapper {
# If no input: exec true # If no input: exit(0)
# If some input: Pass input as input to pipe # If some input: Pass input as input to command on STDIN
# This avoids starting the $read command if there is no input. # This avoids starting the command if there is no input.
# Input:
# $command = command to pipe data to
# Returns: # Returns:
# $cmd = script to prepend to '| ($real command)' # $wrapped_command = the wrapped command
my $command = shift;
# The $tmpfile might exist if run on a remote system - we accept that risk my $script = '$c="'.::perl_quote_scalar($command).'";'.
my ($dummy_fh, $tmpfile) = ::tmpfile(SUFFIX => ".chr"); ::spacefree(0,q{
# Unlink to avoid leaving files if --dry-run or --sshlogin if(sysread(STDIN, $buf, 1)) {
unlink $tmpfile; open($fh, "|-", $c) || die;
$Global::unlink{$tmpfile} = 1; syswrite($fh, $buf);
my $cmd = while($read = sysread(STDIN, $buf, 32768)) {
# Exit value: syswrite($fh, $buf);
# empty input = true }
# some input = exit val from command close $fh;
# sh -c needed as csh cannot hide stderr exit ($?&127 ? 128+($?&127) : 1+$?>>8)
qq{ sh -c 'dd bs=1 count=1 of=$tmpfile 2>/dev/null'; }. }
qq{ test \! -s "$tmpfile" && rm -f "$tmpfile" && exec true; }. });
qq{ (cat $tmpfile; rm $tmpfile; cat - ) }; ::debug("run",'Empty wrap: perl -e '.::shell_quote_scalar($script)."\n");
return $cmd; return 'perl -e '.::shell_quote_scalar($script);
} }
sub filter_through_compress { sub filter_through_compress {
@ -5772,15 +5773,18 @@ sub filter_through_compress {
my $cattail = cattail(); my $cattail = cattail();
for my $fdno (1,2) { for my $fdno (1,2) {
# The tmpfile is used to tell the reader that the writer has started, # Make a communication file.
# so unlink it to start with. my ($fh, $comfile) = ::tmpfile(SUFFIX => ".pac");
unlink $self->fh($fdno,'name'); close $fh;
my $wpid = open(my $fdw,"|-", "(".empty_input_detector(). # Compressor: (echo > $comfile; compress pipe) > output
"| ($opt::compress_program)) >>". # When the echo is written to $comfile, it is known that output file is opened,
# thus output file can then be removed by the decompressor.
my $wpid = open(my $fdw,"|-", "(echo > $comfile; ".empty_input_wrapper($opt::compress_program).") >".
$self->fh($fdno,'name')) || die $?; $self->fh($fdno,'name')) || die $?;
$self->set_fh($fdno,'w',$fdw); $self->set_fh($fdno,'w',$fdw);
$self->set_fh($fdno,'wpid',$wpid); $self->set_fh($fdno,'wpid',$wpid);
my $rpid = open(my $fdr, "-|", "perl", "-e", $cattail, # Decompressor: open output; -s $comfile > 0: rm $comfile output; decompress output > stdout
my $rpid = open(my $fdr, "-|", "perl", "-e", $cattail, $comfile,
$opt::decompress_program, $wpid, $opt::decompress_program, $wpid,
$self->fh($fdno,'name'),$self->fh($fdno,'unlink')) || die $?; $self->fh($fdno,'name'),$self->fh($fdno,'unlink')) || die $?;
$self->set_fh($fdno,'r',$fdr); $self->set_fh($fdno,'r',$fdr);
@ -6233,8 +6237,8 @@ sub wrapped {
# }' 0 0 0 11 | # }' 0 0 0 11 |
$command = (shift @Global::cat_partials). " | ($command)"; $command = (shift @Global::cat_partials). " | ($command)";
} elsif($opt::pipe) { } elsif($opt::pipe) {
# Prepend EOF-detector to avoid starting $command if EOF. # Wrap with EOF-detector to avoid starting $command if EOF.
$command = empty_input_detector(). "| ($command);"; $command = empty_input_wrapper($command);
} }
if($opt::tmux) { if($opt::tmux) {
# Wrap command with 'tmux' # Wrap command with 'tmux'

View file

@ -765,6 +765,8 @@ B<my_grp1_arg> may be run on either B<myserver1> or B<myserver2>,
B<third_arg> may be run on either B<myserver1> or B<myserver3>, B<third_arg> may be run on either B<myserver1> or B<myserver3>,
but B<arg_for_grp2> will only be run on B<myserver2>. but B<arg_for_grp2> will only be run on B<myserver2>.
See also: B<--sshlogin>.
=item B<-I> I<replace-str> =item B<-I> I<replace-str>
@ -1629,8 +1631,12 @@ seconds.
=item B<-S> I<[@hostgroups/][ncpu/]sshlogin[,[@hostgroups/][ncpu/]sshlogin[,...]]> =item B<-S> I<[@hostgroups/][ncpu/]sshlogin[,[@hostgroups/][ncpu/]sshlogin[,...]]>
=item B<-S> I<@hostgroup>
=item B<--sshlogin> I<[@hostgroups/][ncpu/]sshlogin[,[@hostgroups/][ncpu/]sshlogin[,...]]> =item B<--sshlogin> I<[@hostgroups/][ncpu/]sshlogin[,[@hostgroups/][ncpu/]sshlogin[,...]]>
=item B<--sshlogin> I<@hostgroup>
Distribute jobs to remote computers. The jobs will be run on a list of Distribute jobs to remote computers. The jobs will be run on a list of
remote computers. remote computers.
@ -1638,8 +1644,8 @@ If I<hostgroups> is given, the I<sshlogin> will be added to that
hostgroup. Multiple hostgroups are separated by '+'. The I<sshlogin> hostgroup. Multiple hostgroups are separated by '+'. The I<sshlogin>
will always be added to a hostgroup named the same as I<sshlogin>. will always be added to a hostgroup named the same as I<sshlogin>.
If only the I<hostgroups> is given, only the sshlogins in those If only the I<@hostgroup> is given, only the sshlogins in that
hostgroups will be used. hostgroup will be used. Multiple I<@hostgroup> can be given.
GNU B<parallel> will determine the number of CPU cores on the remote GNU B<parallel> will determine the number of CPU cores on the remote
computers and run the number of jobs as specified by B<-j>. If the computers and run the number of jobs as specified by B<-j>. If the

View file

@ -28,9 +28,9 @@ run_test() {
# Check if it was cleaned up # Check if it was cleaned up
find /tmp -maxdepth 1 | find /tmp -maxdepth 1 |
perl -ne '/\.(tmb|chr|tms|par)$/ and ++$a and print "TMP NOT CLEAN. FOUND: $_".`touch '$script'`;' perl -ne '/\.(tmx|pac|arg|all|log|swp|loa|ssh|df|pip|tmb|chr|tms|par)$/ and ++$a and print "TMP NOT CLEAN. FOUND: $_".`touch '$script'`;'
# May be owned by other users # May be owned by other users
sudo rm -f /tmp/*.{tmb,chr,tms,par} sudo rm -f /tmp/*.{tmx,pac,arg,all,log,swp,loa,ssh,df,pip,tmb,chr,tms,par}
} }
export -f run_test export -f run_test

View file

@ -266,7 +266,6 @@ echo 'bug #44250: pxz complains File format not recognized but decompresses anyw
bug #44250: pxz complains File format not recognized but decompresses anyway bug #44250: pxz complains File format not recognized but decompresses anyway
# The first line dumps core if run from make file. Why?! # The first line dumps core if run from make file. Why?!
stdout parallel --compress --compress-program pxz ls /{} ::: OK-if-missing-file stdout parallel --compress --compress-program pxz ls /{} ::: OK-if-missing-file
Segmentation fault (core dumped)
parallel: Error: pxz failed. parallel: Error: pxz failed.
stdout parallel --compress --compress-program pixz --decompress-program 'pixz -d' ls /{} ::: OK-if-missing-file stdout parallel --compress --compress-program pixz --decompress-program 'pixz -d' ls /{} ::: OK-if-missing-file
ls: cannot access /OK-if-missing-file: No such file or directory ls: cannot access /OK-if-missing-file: No such file or directory

View file

@ -931,7 +931,7 @@ This helps funding further development; and it won't cost you a cent.
If you pay 10000 EUR you should feel free to use GNU Parallel without citing. If you pay 10000 EUR you should feel free to use GNU Parallel without citing.
parallel --version parallel --version
GNU parallel 20150415 GNU parallel 20150416
Copyright (C) 2007,2008,2009,2010,2011,2012,2013,2014,2015 Ole Tange Copyright (C) 2007,2008,2009,2010,2011,2012,2013,2014,2015 Ole Tange
and Free Software Foundation, Inc. and Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
@ -943,7 +943,7 @@ Web site: http://www.gnu.org/software/parallel
When using programs that use GNU Parallel to process data for publication When using programs that use GNU Parallel to process data for publication
please cite as described in 'parallel --bibtex'. please cite as described in 'parallel --bibtex'.
parallel --minversion 20130722 && echo Your version is at least 20130722. parallel --minversion 20130722 && echo Your version is at least 20130722.
20150415 20150416
Your version is at least 20130722. Your version is at least 20130722.
parallel --bibtex parallel --bibtex
Academic tradition requires you to cite works you base your article on. Academic tradition requires you to cite works you base your article on.