pkgsrc-Bugs archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

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



>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 <lx%shellcodes.org@localhost>
+  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 <eliz%gnu.org@localhost>
+  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 <eliz%gnu.org@localhost>
+  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);
+ 




Home | Main Index | Thread Index | Old Index