pkg/60649: editors/emacs21: two unbackported CVEs in etags and copy-file

"[email protected] via gnats" <[email protected]>
Newsgroups gmane.os.netbsd.devel.pkgsrc.bugs
Message-ID <[email protected]>
>Number:         60649
>Category:       pkg
>Synopsis:       editors/emacs21: two unbackported CVEs in etags and copy-file
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    pkg-manager
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Wed Aug 26 17:05:00 +0000 2026
>Originator:     Showta Ishizaki
>Release:        NetBSD 11.0, pkgsrc CVS as of 2026-08-27
>Organization:
>Environment:
System: NetBSD 11.0 amd64
>Description:

	editors/emacs21 (21.4a) and editors/emacs21-nox11 carry two CVEs that were
	fixed upstream but never reached the emacs-21 branch (the oldest branch on
	the mirror is emacs-23).  patch-CVE-2017-14482 is already in the tree; these
	add the other two, plus a gets() cleanup.

	CVE-2022-45939 / CVE-2022-48337 -- etags/ctags paste file names into shell
	commands run through system(3)/popen(3) without quoting.  All three call
	sites are live in 21.4: the -u update loop walks argbuffer[] by current_arg,
	"sort -o %s %s" over the -o output file is reached directly, and the
	decompressor for a compressed input file pastes the file's own name into
	"gzip -d -c NAME" for popen.

	    ctags -u -o 'tags;touch /tmp/pwned' file.c        # -o path
	    etags 'a;touch GOTCHA;.gz'                         # decompressor path

	ran the injected command before the fix and do nothing after it; a normal
	"-o out.tags" and a normal "foo.c.gz" still work.  Backport of upstream
	01a4035c8 "Fix etags local command injection vulnerability" (Bug#59817) and
	8a098f651 "Fix quoting of file names in 'ctags'"; the upstream
	escape_shell_arg_string() is brought to the old source as shell_quote().

	CVE-2017-1000383 -- copy-file creates the destination with creat(...,0666)
	and only chmods it afterwards, so a copy of a private file is briefly
	world-readable and a backup inherits the source's setuid/setgid bits.  A
	04755 source produced a 04755 backup before the fix and a 0755 one after.
	Modern Emacs closes this with with-file-modes ?\700 in backup-buffer-copy
	(files.el) and O_CREAT|O_EXCL plus ~06000 in Fcopy_file; this does the same
	at the C level: create 0600 and mask the chmod with 01777.

	patch-src_xrdb.c replaces gets() with fgets() in the [TESTRM] test driver,
	backporting upstream e90a457c46ca.  gets() is not compiled into the shipped
	binary, so it changes nothing that runs; it is here to match upstream and to
	keep the source compiling with newer toolchains.

	pkg_admin audit already flags emacs21 for CVE-2017-1000383 and
	CVE-2022-45939 (by version).


>How-To-Repeat:

	Build editors/emacs21 (or -nox11).  With the built ctags/etags:

	    printf 'int main(void){return 0;}\n' > file.c
	    ctags -u -o 'tags;touch /tmp/pwned' file.c
	    test -f /tmp/pwned && echo vulnerable
	    gzip -c file.c > 'a;touch GOTCHA;.gz'; etags 'a;touch GOTCHA;.gz'
	    test -f GOTCHA && echo vulnerable

	With the built emacs and backup-by-copying:

	    printf secret > src; chmod 4755 src; umask 077
	    emacs -batch -q --eval '(copy-file "src" "dst" t)'
	    ls -l dst        # setuid, world-readable backup before the fix


>Fix:

	Three new patches -- patch-CVE-2022-45939, patch-CVE-2017-1000383 and
	patch-src_xrdb.c -- against the shared editors/emacs21/patches, so both
	emacs21 and emacs21-nox11 get them.  PKGREVISION goes to 50 and 15.

	Built from source on NetBSD 11.0/amd64; the two exploits above are closed and
	ordinary etags and copy-file use is unchanged.


Index: editors/emacs21/Makefile
===================================================================
RCS file: /cvsroot/pkgsrc/editors/emacs21/Makefile,v
diff -u Makefile
--- editors/emacs21/Makefile
+++ editors/emacs21/Makefile
@@ -1,6 +1,6 @@
 # $NetBSD: Makefile,v 1.51 2025/08/30 22:44:55 wiz Exp $
 
-PKGREVISION=	49
+PKGREVISION=	50
 CATEGORIES=	editors
 
 CONFLICTS+=	emacs21-nox11-[0-9]*
Index: editors/emacs21-nox11/Makefile
===================================================================
RCS file: /cvsroot/pkgsrc/editors/emacs21-nox11/Makefile,v
diff -u Makefile
--- editors/emacs21-nox11/Makefile
+++ editors/emacs21-nox11/Makefile
@@ -1,7 +1,7 @@
 # $NetBSD: Makefile,v 1.15 2021/10/06 15:40:15 nia Exp $
 
 PKGNAME=	emacs21-nox11-${EMACSVERSION}a
-PKGREVISION=	14
+PKGREVISION=	15
 CATEGORIES=	editors
 COMMENT_EXTRA=	: non-X11 version
 
Index: editors/emacs21/distinfo
===================================================================
RCS file: /cvsroot/pkgsrc/editors/emacs21/distinfo,v
diff -u distinfo
--- editors/emacs21/distinfo
+++ editors/emacs21/distinfo
@@ -3,7 +3,9 @@
 BLAKE2s (emacs-21.4a.tar.gz) = 8f66d92cce6f5cc673d615e034803f08ccc900bb041c6dbb98b5050d9250dbc1
 SHA512 (emacs-21.4a.tar.gz) = 6932db498e7b6b904d90f817e335690c5c681510812332c514af1d84d91b9841ea3eff0357429c7bf866a55ab94d03193fc9cdbf403a9fe4b71d2673c23b863e
 Size (emacs-21.4a.tar.gz) = 20403483 bytes
+SHA1 (patch-CVE-2017-1000383) = 44089b5ba550b56421d85edae1ff0b9a9231b483
 SHA1 (patch-CVE-2017-14482) = b0e11974076dcb1cad83841c949d0f35fdaa2ca2
+SHA1 (patch-CVE-2022-45939) = 95bcdeafa760e68b7c6013c91d24cbc785fe3dea
 SHA1 (patch-aa) = a7146ff6cc24de8d0caf56a0bc0ce057f659f536
 SHA1 (patch-ab) = 884b8fe5643b06340948c1f10b7c5d643fad2bf6
 SHA1 (patch-ac) = a2de7619fece50cb42b0e23b1651b5bf742ff69a
@@ -37,4 +39,5 @@
 SHA1 (patch-src_alloc_c) = c9d2c7832b8b48835b81789f938540e3827f7cd8
 SHA1 (patch-src_gmalloc.c) = 3d10e1ae9f9d98495a8da47e6051a51ac18aed10
 SHA1 (patch-src_syntax.c) = c56c3b0d1e215cf56f29349bf7e9bf9cce8d0f02
+SHA1 (patch-src_xrdb.c) = bcfb7b85a0f6488afe55a5abf9a2f0ec57f4c3f3
 SHA1 (patch-xx) = 6e5b55c1738c4e8ef58e45065b55629ec26c9b91
Index: editors/emacs21/patches/patch-CVE-2022-45939
===================================================================
RCS file: editors/emacs21/patches/patch-CVE-2022-45939
diff -N editors/emacs21/patches/patch-CVE-2022-45939
--- /dev/null
+++ editors/emacs21/patches/patch-CVE-2022-45939
+$NetBSD$
+
+CVE-2022-45939 and CVE-2022-48337: etags/ctags paste strings into shell
+commands and hand them to system(3)/popen(3) without quoting.  Backport of
+
+  From 01a4035c869b91c153af9a9132c87adb7669ea1c Mon Sep 17 00:00:00 2001
+  From: lu4nx <[email protected]>
+  Date: Tue, 6 Dec 2022
+  Subject: Fix etags local command injection vulnerability  (Bug#59817)
+
+  From 8a098f6517157ebe2364f08008b44ab49c2d1115 Mon Sep 17 00:00:00 2001
+  From: Eli Zaretskii <[email protected]>
+  Date: Fri, 1 Jul 2022
+  Subject: Fix quoting of file names in 'ctags'
+
+Upstream added escape_shell_arg_string() and quotes every file name it
+passes to the shell.  21.4 predates that by twenty years; this brings the
+same fix to the old K&R source, as shell_quote().
+
+All three call sites are live here.  The per-file update loop walks
+argbuffer[] by current_arg (not optind..argc, which is what makes the same
+loop dead in mule), the "sort ... -o ..." over the -o output file is reached
+directly, and the decompressor for a compressed input file pastes the file's
+own name into "gzip -d -c NAME" for popen -- so a file named "a;cmd.gz"
+reached by "etags *" runs cmd.  On NetBSD 11.0/amd64:
+
+    ctags -u -o 'tags;touch /tmp/pwned' file.c        # -o path
+    etags 'a;touch GOTCHA;.gz'                         # decompressor path
+
+ran the injected command before the fix and do nothing after it.  A normal
+"-o out.tags" and a normal "foo.c.gz" still work.
+
+--- lib-src/etags.c.orig
++++ lib-src/etags.c
+@@ -303,6 +303,7 @@
+ static void error P_((const char *, const char *));
+ static void suggest_asking_for_help P_((void));
+ void fatal P_((char *, char *));
++static char *shell_quote P_((char *));
+ static void pfatal P_((char *));
+ static void add_node P_((node *, node **));
+ 
+@@ -909,6 +910,41 @@
+ #endif /* VMS */
+ 
+ 
++/* Return a copy of STR wrapped in single quotes so that a shell passes it
++   through unchanged.  An embedded single quote is closed, escaped and
++   reopened.
++
++   Without this a string reaches the shell as written, and one that contains
++   shell metacharacters runs whatever it likes.  CVE-2022-45939 and
++   CVE-2022-48337.  */
++
++static char *
++shell_quote (str)
++     char *str;
++{
++  char *p, *q, *out;
++  int n = 0;
++
++  for (p = str; *p != '\0'; p++)
++    n += (*p == '\'') ? 4 : 1;
++  out = xnew (n + 3, char);
++  q = out;
++  *q++ = '\'';
++  for (p = str; *p != '\0'; p++)
++    if (*p == '\'')
++      {
++	*q++ = '\'';
++	*q++ = '\\';
++	*q++ = '\'';
++	*q++ = '\'';
++      }
++    else
++      *q++ = *p;
++  *q++ = '\'';
++  *q = '\0';
++  return out;
++}
++
+ int
+ main (argc, argv)
+      int argc;
+@@ -1195,16 +1231,25 @@
+ 
+   if (update)
+     {
+-      char cmd[BUFSIZ];
+       for (i = 0; i < current_arg; ++i)
+ 	{
++	  char *pattern, *qpattern, *qtagfile, *cmd;
++
+ 	  if (argbuffer[i].arg_type != at_filename)
+ 	    continue;
+-	  sprintf (cmd,
+-		   "mv %s OTAGS;fgrep -v '\t%s\t' OTAGS >%s;rm OTAGS",
+-		   tagfile, argbuffer[i].what, tagfile);
++	  pattern = xnew (strlen (argbuffer[i].what) + 3, char);
++	  sprintf (pattern, "\t%s\t", argbuffer[i].what);
++	  qpattern = shell_quote (pattern);
++	  qtagfile = shell_quote (tagfile);
++	  cmd = xnew (2 * strlen (qtagfile) + strlen (qpattern) + 32, char);
++	  sprintf (cmd, "mv %s OTAGS;fgrep -v %s OTAGS >%s;rm OTAGS",
++		   qtagfile, qpattern, qtagfile);
+ 	  if (system (cmd) != GOOD)
+ 	    fatal ("failed to execute shell command", (char *)NULL);
++	  free (pattern);
++	  free (qpattern);
++	  free (qtagfile);
++	  free (cmd);
+ 	}
+       append_to_tagfile = TRUE;
+     }
+@@ -1219,9 +1264,15 @@
+ 
+   if (update)
+     {
+-      char cmd[2*BUFSIZ+10];
+-      sprintf (cmd, "sort -o %.*s %.*s", BUFSIZ, tagfile, BUFSIZ, tagfile);
+-      exit (system (cmd));
++      char *qtagfile = shell_quote (tagfile);
++      char *cmd = xnew (2 * strlen (qtagfile) + 16, char);
++      int rc;
++
++      sprintf (cmd, "sort %s -o %s", qtagfile, qtagfile);
++      rc = system (cmd);
++      free (qtagfile);
++      free (cmd);
++      exit (rc);
+     }
+   return GOOD;
+ }
+@@ -1455,8 +1506,13 @@
+     }
+   if (real_name == compressed_name)
+     {
+-      char *cmd = concat (compr->command, " ", real_name);
++      /* Quote the file name: it is pasted into a shell command run by
++	 popen, so a name like "x;cmd.gz" would otherwise run cmd.
++	 CVE-2022-45939 / CVE-2022-48337.  */
++      char *qname = shell_quote (real_name);
++      char *cmd = concat (compr->command, " ", qname);
+       inf = (FILE *) popen (cmd, "r");
++      free (qname);
+       free (cmd);
+     }
+   else
Index: editors/emacs21/patches/patch-CVE-2017-1000383
===================================================================
RCS file: editors/emacs21/patches/patch-CVE-2017-1000383
diff -N editors/emacs21/patches/patch-CVE-2017-1000383
--- /dev/null
+++ editors/emacs21/patches/patch-CVE-2017-1000383
+$NetBSD$
+
+CVE-2017-1000383: copy-file, which backup-buffer uses to make backups when
+backup-by-copying is in effect, creates the destination with creat(...,0666)
+and only chmods it to the wanted mode afterwards.  A copy of a private file
+is briefly world-readable, and a backup inherits the source's setuid/setgid
+bits.
+
+Upstream closes this in two places that 20.7/21.4 predate: lisp/files.el's
+backup-buffer-copy wraps the copy in (with-file-modes ?\700 ...) -- the
+with-file-modes macro is
+
+  From d63d883a97e32aa5f0983b6577f7cbef8b6c3a3d Mon Sep 17 00:00:00 2001
+  Date: Wed, 14 May 2014
+  Subject: Add with-file-modes macro, and use it
+
+-- and modern src/fileio.c creates the copy with O_CREAT|O_EXCL and strips
+the setuid/setgid bits (preserved_permissions &= ~06000).  This backport
+does the same at the C level for the old source: create 0600 so the file is
+never group/other-readable while it is being written, and mask the trailing
+chmod with 01777 (keeping the sticky and rwx bits, dropping setuid/setgid --
+the same effect as #o1777 in backup-buffer-copy).
+
+Reproduced on NetBSD 11.0/amd64 with emacs -batch and backup-by-copying: a
+04755 source produced a 04755 (setuid) backup before the fix and a 0755 one
+after.  This is the same change already carried in zakinko/mule.
+
+--- src/fileio.c.orig
++++ src/fileio.c
+@@ -2417,7 +2417,11 @@
+   /* System's default file type was set to binary by _fmode in emacs.c.  */
+   ofd = creat (XSTRING (encoded_newname)->data, S_IREAD | S_IWRITE);
+ #else /* not MSDOS */
+-  ofd = creat (XSTRING (encoded_newname)->data, 0666);
++  /* Create the copy owner-readable/writable only.  The final chmod below
++     widens it to the intended mode once the data is in place; creating it
++     0666 (masked only by umask) meant a copy of a private file was briefly
++     world-readable while it was being written.  CVE-2017-1000383.  */
++  ofd = creat (XSTRING (encoded_newname)->data, 0600);
+ #endif /* not MSDOS */
+ #endif /* VMS */
+   if (ofd < 0)
+@@ -2450,7 +2454,11 @@
+ 			    Fcons (newname, Qnil)));
+ 	}
+ #ifndef MSDOS
+-      chmod (XSTRING (encoded_newname)->data, st.st_mode & 07777);
++      /* Mask off the setuid and setgid bits: a copy -- above all a backup
++	 file -- must never inherit them from the original.  01777 keeps the
++	 sticky bit and the rwx bits, matching what a modern Emacs does with
++	 #o1777.  CVE-2017-1000383.  */
++      chmod (XSTRING (encoded_newname)->data, st.st_mode & 01777);
+ #else /* MSDOS */
+ #if defined (__DJGPP__) && __DJGPP__ > 1
+       /* In DJGPP v2.0 and later, fstat usually returns true file mode bits,
Index: editors/emacs21/patches/patch-src_xrdb.c
===================================================================
RCS file: editors/emacs21/patches/patch-src_xrdb.c
diff -N editors/emacs21/patches/patch-src_xrdb.c
--- /dev/null
+++ editors/emacs21/patches/patch-src_xrdb.c
+$NetBSD$
+
+Replace gets() in the [TESTRM] test driver with fgets(), backporting
+
+  From e90a457c46ca4c6a231a422b9b30f5e0d0f9d1c1 Mon Sep 17 00:00:00 2001
+  From: Eli Zaretskii <[email protected]>
+  Date: Thu, 8 Sep 2022
+  Subject: * src/xrdb.c (main) [TESTRM]: Replace gets with fgets.
+
+gets() has no bound and cannot be used safely; it was removed from C11.
+The call is inside the TESTRM stand-alone driver, which is not compiled
+into Emacs (the built binary has no gets symbol), so this changes no
+shipped code -- it is here to match upstream and to keep the source
+clean, since gets() is a hard error with newer toolchains.
+
+--- src/xrdb.c.orig
++++ src/xrdb.c
+@@ -788,14 +788,14 @@
+       char query_class[90];
+ 
+       printf ("Name: ");
+-      gets (query_name);
++      fgets (query_name, 90, stdin);
+ 
+       if (strlen (query_name))
+ 	{
+ 	  char *value;
+ 
+ 	  printf ("Class: ");
+-	  gets (query_class);
++	  fgets (query_class, 90, stdin);
+ 
+ 	  value = x_get_string_resource (xdb, query_name, query_class);
+
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.