Commit df38fe8e48528c82e16e7bcced38eeab5bcf9d7c

Authored by Jay Berkenbilt
1 parent 3cacb27a

Fix string bounds checking in completion code (fixes #441)

ChangeLog
  1 +2021-05-12 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Bug fix: ensure we don't overflow any string bounds while
  4 + handling completion, even when we are given bogus input values.
  5 + Fixes #441.
  6 +
1 2021-05-09 Jay Berkenbilt <ejb@ql.org> 7 2021-05-09 Jay Berkenbilt <ejb@ql.org>
2 8
3 * Improve performance of preservation of object streams by 9 * Improve performance of preservation of object streams by
qpdf/qpdf.cc
@@ -3469,16 +3469,27 @@ ArgParser::checkCompletion() @@ -3469,16 +3469,27 @@ ArgParser::checkCompletion()
3469 { 3469 {
3470 // See if we're being invoked from bash completion. 3470 // See if we're being invoked from bash completion.
3471 std::string bash_point_env; 3471 std::string bash_point_env;
3472 - if (QUtil::get_env("COMP_LINE", &bash_line) &&  
3473 - QUtil::get_env("COMP_POINT", &bash_point_env)) 3472 + // On Windows with mingw, there have been times when there appears
  3473 + // to be no way to distinguish between an empty environment
  3474 + // variable and an unset variable. There are also conditions under
  3475 + // which bash doesn't set COMP_LINE. Therefore, enter this logic
  3476 + // if either COMP_LINE or COMP_POINT are set. They will both be
  3477 + // set together under ordinary circumstances.
  3478 + bool got_line = QUtil::get_env("COMP_LINE", &bash_line);
  3479 + bool got_point = QUtil::get_env("COMP_POINT", &bash_point_env);
  3480 + if (got_line || got_point)
3474 { 3481 {
3475 size_t p = QUtil::string_to_uint(bash_point_env.c_str()); 3482 size_t p = QUtil::string_to_uint(bash_point_env.c_str());
3476 - if ((p > 0) && (p <= bash_line.length())) 3483 + if (p < bash_line.length())
3477 { 3484 {
3478 // Truncate the line. We ignore everything at or after the 3485 // Truncate the line. We ignore everything at or after the
3479 // cursor for completion purposes. 3486 // cursor for completion purposes.
3480 bash_line = bash_line.substr(0, p); 3487 bash_line = bash_line.substr(0, p);
3481 } 3488 }
  3489 + if (p > bash_line.length())
  3490 + {
  3491 + p = bash_line.length();
  3492 + }
3482 // Set bash_cur and bash_prev based on bash_line rather than 3493 // Set bash_cur and bash_prev based on bash_line rather than
3483 // relying on argv. This enables us to use bashcompinit to get 3494 // relying on argv. This enables us to use bashcompinit to get
3484 // completion in zsh too since bashcompinit sets COMP_LINE and 3495 // completion in zsh too since bashcompinit sets COMP_LINE and
@@ -3489,8 +3500,9 @@ ArgParser::checkCompletion() @@ -3489,8 +3500,9 @@ ArgParser::checkCompletion()
3489 // for the first separator. bash_cur is everything after the 3500 // for the first separator. bash_cur is everything after the
3490 // last separator, possibly empty. 3501 // last separator, possibly empty.
3491 char sep(0); 3502 char sep(0);
3492 - while (--p > 0) 3503 + while (p > 0)
3493 { 3504 {
  3505 + --p;
3494 char ch = bash_line.at(p); 3506 char ch = bash_line.at(p);
3495 if ((ch == ' ') || (ch == '=') || (ch == ':')) 3507 if ((ch == ' ') || (ch == '=') || (ch == ':'))
3496 { 3508 {
@@ -3498,7 +3510,10 @@ ArgParser::checkCompletion() @@ -3498,7 +3510,10 @@ ArgParser::checkCompletion()
3498 break; 3510 break;
3499 } 3511 }
3500 } 3512 }
3501 - bash_cur = bash_line.substr(1+p, std::string::npos); 3513 + if (1+p <= bash_line.length())
  3514 + {
  3515 + bash_cur = bash_line.substr(1+p, std::string::npos);
  3516 + }
3502 if ((sep == ':') || (sep == '=')) 3517 if ((sep == ':') || (sep == '='))
3503 { 3518 {
3504 // Bash sets prev to the non-space separator if any. 3519 // Bash sets prev to the non-space separator if any.
@@ -3512,8 +3527,9 @@ ArgParser::checkCompletion() @@ -3512,8 +3527,9 @@ ArgParser::checkCompletion()
3512 // Go back to the last separator and set prev based on 3527 // Go back to the last separator and set prev based on
3513 // that. 3528 // that.
3514 size_t p1 = p; 3529 size_t p1 = p;
3515 - while (--p1 > 0) 3530 + while (p1 > 0)
3516 { 3531 {
  3532 + --p1;
3517 char ch = bash_line.at(p1); 3533 char ch = bash_line.at(p1);
3518 if ((ch == ' ') || (ch == ':') || (ch == '=')) 3534 if ((ch == ' ') || (ch == ':') || (ch == '='))
3519 { 3535 {
qpdf/qtest/qpdf.test
@@ -86,6 +86,10 @@ $td-&gt;runtest(&quot;UTF-16 encoding errors&quot;, @@ -86,6 +86,10 @@ $td-&gt;runtest(&quot;UTF-16 encoding errors&quot;,
86 $td->NORMALIZE_NEWLINES); 86 $td->NORMALIZE_NEWLINES);
87 87
88 my @completion_tests = ( 88 my @completion_tests = (
  89 + ['', 0, 'bad-input-1'],
  90 + ['', 1, 'bad-input-2'],
  91 + ['', 2, 'bad-input-3'],
  92 + ['qpdf', 2, 'bad-input-4'],
89 ['qpdf ', undef, 'top'], 93 ['qpdf ', undef, 'top'],
90 ['qpdf -', undef, 'top-arg'], 94 ['qpdf -', undef, 'top-arg'],
91 ['qpdf --enc', undef, 'enc'], 95 ['qpdf --enc', undef, 'enc'],
@@ -5229,8 +5233,13 @@ sub bash_completion @@ -5229,8 +5233,13 @@ sub bash_completion
5229 $point = length($line); 5233 $point = length($line);
5230 } 5234 }
5231 my $before_point = substr($line, 0, $point); 5235 my $before_point = substr($line, 0, $point);
5232 - $before_point =~ m/^(.*)([ =])([^= ]*)$/ or die;  
5233 - my ($first, $sep, $cur) = ($1, $2, $3); 5236 + my $first = '';
  5237 + my $sep = '';
  5238 + my $cur = '';
  5239 + if ($before_point =~ m/^(.*)([ =])([^= ]*)$/)
  5240 + {
  5241 + ($first, $sep, $cur) = ($1, $2, $3);
  5242 + }
5234 my $prev = ($sep eq '=' ? $sep : $first); 5243 my $prev = ($sep eq '=' ? $sep : $first);
5235 $prev =~ s/.* (\S+)$/$1/; 5244 $prev =~ s/.* (\S+)$/$1/;
5236 my $this = $first; 5245 my $this = $first;
qpdf/qtest/qpdf/completion-bad-input-1.out 0 → 100644
  1 +!
qpdf/qtest/qpdf/completion-bad-input-2.out 0 → 100644
  1 +!
qpdf/qtest/qpdf/completion-bad-input-3.out 0 → 100644
  1 +!
qpdf/qtest/qpdf/completion-bad-input-4.out 0 → 100644
  1 +!