grep: disable lookahead on error

regexec(3) can fail. E.g. on macOS it fails if it is used with an UTF-8 locale to match a valid regex against a buffer containing invalid UTF-8 characters. git grep has two ways to search for matches in a file: Either it splits its contents into lines and matches them separately, or it matches the whole content and figures out line boundaries later. The latter is done by look_ahead() and it's quicker in the common case where most files don't contain a match. Fall back to line-by-line matching if look_ahead() encounters an regexec(3) error by propagating errors out of patmatch() and bailing out of look_ahead() if there is one. This way we at least can find matches in lines that contain only valid characters. That matches the behavior of grep(1) on macOS. pcre2match() dies if pcre2_jit_match() or pcre2_match() fail, but since we use the flag PCRE2_MATCH_INVALID_UTF it handles invalid UTF-8 characters gracefully. So implement the fall-back only for regexec(3) and leave the PCRE2 matching unchanged. Reported-by: David Gstir <david@sigma-star.at> Signed-off-by: René Scharfe <l.s.r@web.de> Tested-by: David Gstir <david@sigma-star.at> Signed-off-by: Taylor Blau <me@ttaylorr.com>

René Scharfe committed Oct 20, 2024 at 13:02 UTC ce025ae4f61e8e32b2ae6589e43e03e60f713f2d
2 files changed +29 -10
grep.c
+20 -10
@@ -906,15 +906,17 @@ static int patmatch(struct grep_pat *p,
906 const char *line, const char *eol,
907 regmatch_t *match, int eflags)
908 {
909 - int hit;
910 -
909 if (p->pcre2_pattern)
912 - hit = !pcre2match(p, line, eol, match, eflags);
913 - else
914 - hit = !regexec_buf(&p->regexp, line, eol - line, 1, match,
915 - eflags);
910 + return !pcre2match(p, line, eol, match, eflags);
911
917 - return hit;
912 + switch (regexec_buf(&p->regexp, line, eol - line, 1, match, eflags)) {
913 + case 0:
914 + return 1;
915 + case REG_NOMATCH:
916 + return 0;
917 + default:
918 + return -1;
919 + }
920 }
921
922 static void strip_timestamp(const char *bol, const char **eol_p)
@@ -952,6 +954,8 @@ static int headerless_match_one_pattern(struct grep_pat *p,
954
955 again:
956 hit = patmatch(p, bol, eol, pmatch, eflags);
957 + if (hit < 0)
958 + hit = 0;
959
960 if (hit && p->word_regexp) {
961 if ((pmatch[0].rm_so < 0) ||
@@ -1461,6 +1465,8 @@ static int look_ahead(struct grep_opt *opt,
1465 regmatch_t m;
1466
1467 hit = patmatch(p, bol, bol + *left_p, &m, 0);
1468 + if (hit < 0)
1469 + return -1;
1470 if (!hit || m.rm_so < 0 || m.rm_eo < 0)
1471 continue;
1472 if (earliest < 0 || m.rm_so < earliest)
@@ -1655,9 +1661,13 @@ static int grep_source_1(struct grep_opt *opt, struct grep_source *gs, int colle
1661 if (try_lookahead
1662 && !(last_hit
1663 && (show_function ||
1658 - lno <= last_hit + opt->post_context))
1659 - && look_ahead(opt, &left, &lno, &bol))
1660 - break;
1664 + lno <= last_hit + opt->post_context))) {
1665 + hit = look_ahead(opt, &left, &lno, &bol);
1666 + if (hit < 0)
1667 + try_lookahead = 0;
1668 + else if (hit)
1669 + break;
1670 + }
1671 eol = end_of_line(bol, &left);
1672
1673 if ((ctx == GREP_CONTEXT_HEAD) && (eol == bol))
t/t7810-grep.sh
+9
@@ -87,6 +87,7 @@ test_expect_success setup '
87 # Still a no-op.
88 function dummy() {}
89 EOF
90 + printf "\200\nASCII\n" >invalid-utf8 &&
91 if test_have_prereq FUNNYNAMES
92 then
93 echo unusual >"\"unusual\" pathname" &&
@@ -534,6 +535,14 @@ do
535 test_cmp expected actual
536 '
537
538 + test_expect_success "grep $L searches past invalid lines on UTF-8 locale" '
539 + LC_ALL=en_US.UTF-8 git grep A. invalid-utf8 >actual &&
540 + cat >expected <<-EOF &&
541 + invalid-utf8:ASCII
542 + EOF
543 + test_cmp expected actual
544 + '
545 +
546 test_expect_success FUNNYNAMES "grep $L should quote unusual pathnames" '
547 cat >expected <<-EOF &&
548 ${HC}"\"unusual\" pathname":unusual