Fix ctags command execution vulnerability (CVE-2022-45939) Backported from emacs-29 branch https://bugs.gentoo.org/883687 commit d48bb4874bc6cd3e69c7a15fc3c91cc141025c51 Author: Xi Lu Date: Fri Nov 25 14:38:29 2022 +0800 Fixed ctags local command execute vulnerability --- emacs-28.2/lib-src/etags.c +++ emacs-28.2/lib-src/etags.c @@ -382,7 +382,7 @@ static language *get_language_from_langname (const char *); static void readline (linebuffer *, FILE *); -static ptrdiff_t readline_internal (linebuffer *, FILE *, char const *); +static ptrdiff_t readline_internal (linebuffer *, FILE *, char const *, const bool); static bool nocase_tail (const char *); static void get_tag (char *, char **); static void get_lispy_tag (char *); @@ -406,7 +406,9 @@ static void pfnote (char *, bool, char *, ptrdiff_t, intmax_t, intmax_t); static void invalidate_nodes (fdesc *, node **); static void put_entries (node *); +static void clean_matched_file_tag (char const * const, char const * const); +static void do_move_file (const char *, const char *); static char *concat (const char *, const char *, const char *); static char *skip_spaces (char *); static char *skip_non_spaces (char *); @@ -1339,7 +1341,7 @@ if (parsing_stdin) fatal ("cannot parse standard input " "AND read file names from it"); - while (readline_internal (&filename_lb, stdin, "-") > 0) + while (readline_internal (&filename_lb, stdin, "-", false) > 0) process_file_name (filename_lb.buffer, lang); } else @@ -1387,9 +1389,6 @@ /* From here on, we are in (CTAGS && !cxref_style) */ if (update) { - char *cmd = - xmalloc (strlen (tagfile) + whatlen_max + - sizeof "mv..OTAGS;grep -Fv '\t\t' OTAGS >;rm OTAGS"); for (i = 0; i < current_arg; ++i) { switch (argbuffer[i].arg_type) @@ -1400,17 +1399,8 @@ default: continue; /* the for loop */ } - char *z = stpcpy (cmd, "mv "); - z = stpcpy (z, tagfile); - z = stpcpy (z, " OTAGS;grep -Fv '\t"); - z = stpcpy (z, argbuffer[i].what); - z = stpcpy (z, "\t' OTAGS >"); - z = stpcpy (z, tagfile); - strcpy (z, ";rm OTAGS"); - if (system (cmd) != EXIT_SUCCESS) - fatal ("failed to execute shell command"); + clean_matched_file_tag (tagfile, argbuffer[i].what); } - free (cmd); append_to_tagfile = true; } @@ -1439,6 +1429,51 @@ return EXIT_SUCCESS; } +/* + * Equivalent to: mv tags OTAGS;grep -Fv ' filename ' OTAGS >tags;rm OTAGS + */ +static void +clean_matched_file_tag (const char* tagfile, const char* match_file_name) +{ + FILE *otags_f = fopen ("OTAGS", "wb"); + FILE *tag_f = fopen (tagfile, "rb"); + + if (otags_f == NULL) + pfatal ("OTAGS"); + + if (tag_f == NULL) + pfatal (tagfile); + + int buf_len = strlen (match_file_name) + sizeof ("\t\t ") + 1; + char *buf = xmalloc (buf_len); + snprintf (buf, buf_len, "\t%s\t", match_file_name); + + linebuffer line; + linebuffer_init (&line); + while (readline_internal (&line, tag_f, tagfile, true) > 0) + { + if (ferror (tag_f)) + pfatal (tagfile); + + if (strstr (line.buffer, buf) == NULL) + { + fprintf (otags_f, "%s\n", line.buffer); + if (ferror (tag_f)) + pfatal (tagfile); + } + } + free (buf); + free (line.buffer); + + if (fclose (otags_f) == EOF) + pfatal ("OTAGS"); + + if (fclose (tag_f) == EOF) + pfatal (tagfile); + + do_move_file ("OTAGS", tagfile); + return; +} /* * Return a compressor given the file name. If EXTPTR is non-zero, @@ -1822,7 +1857,7 @@ /* Else look for sharp-bang as the first two characters. */ if (parser == NULL - && readline_internal (&lb, inf, infilename) > 0 + && readline_internal (&lb, inf, infilename, false) > 0 && lb.len >= 2 && lb.buffer[0] == '#' && lb.buffer[1] == '!') @@ -6861,7 +6896,7 @@ if (regexfp == NULL) pfatal (regexfile); linebuffer_init (®exbuf); - while (readline_internal (®exbuf, regexfp, regexfile) > 0) + while (readline_internal (®exbuf, regexfp, regexfile, false) > 0) analyze_regex (regexbuf.buffer); free (regexbuf.buffer); if (fclose (regexfp) != 0) @@ -7209,11 +7244,13 @@ /* * Read a line of text from `stream' into `lbp', excluding the - * newline or CR-NL, if any. Return the number of characters read from - * `stream', which is the length of the line including the newline. + * newline or CR-NL (if `leave_cr` is false), if any. Return the + * number of characters read from `stream', which is the length + * of the line including the newline. * - * On DOS or Windows we do not count the CR character, if any before the - * NL, in the returned length; this mirrors the behavior of Emacs on those + * On DOS or Windows, if `leave_cr` is false, we do not count the + * CR character, if any before the NL, in the returned length; + * this mirrors the behavior of Emacs on those * platforms (for text files, it translates CR-NL to NL as it reads in the * file). * @@ -7221,7 +7258,7 @@ * appended to `filebuf'. */ static ptrdiff_t -readline_internal (linebuffer *lbp, FILE *stream, char const *filename) +readline_internal (linebuffer *lbp, FILE *stream, char const *filename, const bool leave_cr) { char *buffer = lbp->buffer; char *p = lbp->buffer; @@ -7251,19 +7288,19 @@ break; } if (c == '\n') - { - if (p > buffer && p[-1] == '\r') - { - p -= 1; - chars_deleted = 2; - } - else - { - chars_deleted = 1; - } - *p = '\0'; - break; - } + { + if (!leave_cr && p > buffer && p[-1] == '\r') + { + p -= 1; + chars_deleted = 2; + } + else + { + chars_deleted = 1; + } + *p = '\0'; + break; + } *p++ = c; } lbp->len = p - buffer; @@ -7294,7 +7331,7 @@ readline (linebuffer *lbp, FILE *stream) { linecharno = charno; /* update global char number of line start */ - ptrdiff_t result = readline_internal (lbp, stream, infilename); + ptrdiff_t result = readline_internal (lbp, stream, infilename, false); lineno += 1; /* increment global line number */ charno += result; /* increment global char number */ @@ -7652,6 +7689,46 @@ return templt; } +static void +do_move_file(const char *src_file, const char *dst_file) +{ + if (rename (src_file, dst_file) == 0) + return; + + FILE *src_f = fopen (src_file, "rb"); + FILE *dst_f = fopen (dst_file, "wb"); + + if (src_f == NULL) + pfatal (src_file); + + if (dst_f == NULL) + pfatal (dst_file); + + int c; + while ((c = fgetc (src_f)) != EOF) + { + if (ferror (src_f)) + pfatal (src_file); + + if (ferror (dst_f)) + pfatal (dst_file); + + if (fputc (c, dst_f) == EOF) + pfatal ("cannot write"); + } + + if (fclose (src_f) == EOF) + pfatal (src_file); + + if (fclose (dst_f) == EOF) + pfatal (dst_file); + + if (unlink (src_file) == -1) + pfatal ("unlink error"); + + return; +} + /* Return a newly allocated string containing the file name of FILE relative to the absolute directory DIR (which should end with a slash). */ static char *