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

Bug#987975: unblock: mksh/59c-6



Package: release.debian.org
Severity: normal
User: release.debian.org@packages.debian.org
Usertags: unblock
X-Debbugs-Cc: tg@mirbsd.de

Please unblock package mksh

Give it a few days in sid first, please, but I’m filing the
unblock request right now already so it has more time for review.

[ Reason ]
These are cherry-picks, reduced to the minimum necessary (e.g.
less RCS ID churn, some changes that aren’t _that_ crucial removed
or modified in a way to make the diff smaller), that address the
following things:
• some memory leaks
• harden conversion of imported variables to integer, similar to
  Perl’s “taint”; this is basically what ksh93 had as CVE-2019-14868
• fix lexer token state corruption when switching back and forth
  between command line editing and lexing (could be triggered by an
  editing command reliably)
• fix escaping (typeset -p; ${var@Q}) and globbing (tab completion,
  pattern expansion) for pathnames which contain escaped or unescaped
  curly braces, tilde, \x02
Furthermore because this is a new sourceful upload the buildds will
rebuild this against latest linux-libc-dev and klibc (I checked, the
unblock on the latter was already granted) which is a good thing,
especially given klibc’s malloc fixes.

The attached diff is a debdiff in which the single-debian-patch
was replaced by a diff between the patched extracted trees to
increase legibility, as usual. To aid in mapping the patch hunks
to the topics above I will comment on them here:

 mksh-59c/debian/changelog |   25 +++++++++++++

- change documentation

 mksh_59c-6/check.t        |   37 +++++++++++++++++++

- changed version date to 2021/05/03
- new test for the “taint” checks

 mksh_59c-6/edit.c         |   73 ++++++++++++++++++++++++++-------------

- only escaping-related changes

 mksh_59c-6/eval.c         |    6 +--

- memory leak fixes: s/global(arrayname(x))/arraybase(x)/

 mksh_59c-6/misc.c         |    1 

- another memory leak, in command line option -T processing

 mksh_59c-6/mksh.1         |    4 +-

- documentation for the escaping-related changes

 mksh_59c-6/mksh.faq       |   18 ++++++++-

- remove note about evaluate-region being broken (lexer state thing)
- documentation for the “taint” thing

 mksh_59c-6/sh.h           |   14 +++----

- version number
- escaping-related changes
- arrayname ⇒ arraybase (prototype)

 mksh_59c-6/shf.c          |    2 -

- escaping-related

 mksh_59c-6/syn.c          |    4 +-

- lexer state issue

 mksh_59c-6/var.c          |   86 ++++++++++++++++++++++++++++++----------------

- the @@ -938,7 +944,7 @@ hunk and the last one are memory
  leak-related (arrayname ⇒ arraybase; arraybase implementation
  free()s the temporary variable after calling global() properly
  and uses a stack buffer for small values to reduce memory
  allocator pressure)
- all others: “taint”: factor out checking a string for being
  a valid integer from getint() to getnum() and call getnum()
  when converting a variable that was imported from the environment
  to integer to check if it is purely numeric

 11 files changed, 200 insertions(+), 70 deletions(-)


[ Impact ]
- the evaluate-region editing command is still broken
  (I can’t think of any other obvious codepath that triggers
  the lexer issue)
- environment variables COLUMNS, LINES, SECONDS, etc. can be
  used for shell DoS, possibly command execution
- memory leaks (minor, most “lost” blocks are cleaned up when
  a scope is left… if it is even left)
- files with \x02 in them possibly can’t be tab-completed
- pathnames with tildes in them tabcomplete to user homedirs
  instead; same with pattern expansion: 'echo \~bar/*'
- variables with {} or ~ in their values are escaped without
  quoting these characters making them not safe for re-entry
  into the shell (possibly changing their value); this also
  affects internal pass-escaped-values-around like typeset -f
  or even $(…)

[ Tests ]
- the “taint” thing has an automated test
- I wrote excessive code to test the changes to the character
  classes, this is about 600 LoC, which I omitted here to keep
  the diff small, as it’d be called manually while developing
  anyway
- most nōn-interactive functionality is covered by the testsuite
  (which is also run via autopkgtests)
- I tested the escaping and tab-completion things as well as
  the lexer / evaluate-region thing manually

I also did a full rebuild of MirBSD itself with the changes
applied which exercises the shell quite a bit.

[ Risks ]
- the “taint” thing changes the way some scripts may operate,
  but the previous behaviour was unsafe and other shells are
  the same; mksh is de-facto leaf in Debian (I looked at shunit2
  for the previous unblock, and it’ll basically ignore mksh);
  I guess this is medium risk for mksh (which is why this should
  simmer in sid for a while first, though my users tend to find
  bugs rather quickly) but low risk for Debian or the release
  overall considering its virtually-leafness
- the escaping changes only add backslashes that are safe (extra
  backslashes in those cases can’t hurt anyway, eg. = is already
  almost needlessly escaped)
- the memory leaks are low-risk as “obvious”
- same for the lexer change

[ Checklist ]
  [x] all changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in testing

[ Other info ]
(let it simmer in sid for a few days first)

unblock mksh/59c-6
diff -Nru mksh-59c/debian/changelog mksh-59c/debian/changelog
--- mksh-59c/debian/changelog	2021-03-13 19:09:48.000000000 +0100
+++ mksh-59c/debian/changelog	2021-05-03 03:26:28.000000000 +0200
@@ -1,3 +1,28 @@
+mksh (59c-6) unstable; urgency=medium
+
+  * Clear “taint” on most actions mutating a variable
+
+ -- Thorsten Glaser <tg@mirbsd.de>  Mon, 03 May 2021 03:26:28 +0200
+
+mksh (59c-5) unstable; urgency=medium
+
+  * Apply targeted fixes, intended for bullseye:
+    - [tg] Plug some memory leaks
+    - [tg] Harden conversion of imported variables to integer, like
+      Perl “taint”: imported variables will now lose the value when
+      converting to integer but they are not purely numeric
+      (CVE-2019-14868 was a similar issue in AT&T ksh); honour
+      -o posix for leading-zero as octal, though (but continue not
+      when importing array indicēs)
+    - [tg] Fix lexer token state corruption when reading new input;
+      makes evaluate-region editing command actually useful
+    - [tg] Fix proper escaping/quoting and tab completion for tilde,
+      curly braces and \x02, either escaped or not
+    Note: patches are reduced to minimum change for bullseye
+  * Rebuild for outdated Built-Using
+
+ -- Thorsten Glaser <tg@mirbsd.de>  Sun, 02 May 2021 23:52:52 +0200
+
 mksh (59c-4) unstable; urgency=low
 
   * Update to upstream CVS HEAD
diff -pruN mksh_59c-4/check.t mksh_59c-6/check.t
--- mksh_59c-4/check.t	2021-05-03 00:29:50.000000000 +0200
+++ mksh_59c-6/check.t	2021-05-03 03:57:57.000000000 +0200
@@ -31,7 +31,7 @@
 # (2013/12/02 20:39:44) http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/regress/bin/ksh/?sortby=date
 
 expected-stdout:
-	KSH R59 2021/03/13
+	KSH R59 2021/05/03
 description:
 	Check base version of full shell
 stdin:
@@ -13230,6 +13230,41 @@ expected-stdout:
 	2=\x7Cfoo-e \x4B
 	3=\x7Cfoo-e \x4B
 ---
+name: env-intvars
+description:
+	Check that importing integers fails except for numbers
+stdin:
+	unset foo bar
+	print 1 $foo , $(typeset -p bar) .
+	print 2 $(foo=123 "$__progname" -c 'integer foo; print -- $foo' 2>&1) , \
+	    $(env 'bar[123]=baz' "$__progname" -c 'typeset -p bar') .
+	print 3 $(foo='abc[$(echo >&2 fowled)0]' "$__progname" -c 'integer foo; print -- $foo' 2>&1) , \
+	    $(env 'bar[$(echo >&2 fowled)0]=baz' "$__progname" -c 'typeset -p bar') .
+	print 4 $(foo=0123 "$__progname" +o posix -c 'integer foo; print -- $foo' 2>&1) , \
+	    $(env 'bar[0123]=baz' "$__progname" +o posix -c 'typeset -p bar') .
+	# ksh93 does not do this:
+	print 5 $(foo=0123 "$__progname" -o posix -c 'integer foo; print -- $foo' 2>&1) .
+	# at import time FPOSIX is not yet set
+	print 6 $(foo=0x123 "$__progname" -c 'integer foo; print -- $foo' 2>&1) , \
+	    $(env 'bar[0x123]=baz' "$__progname" -c 'typeset -p bar') .
+	print 7 $(foo=12#123 "$__progname" -c 'integer foo; print -- $foo' 2>&1) , \
+	    $(env 'bar[12#123]=baz' "$__progname" -c 'typeset -p bar') .
+	print 8 $(foo=1+1 "$__progname" -c 'integer foo; print -- $foo' 2>&1) , \
+	    $(env 'bar[1+1]=baz' "$__progname" -c 'typeset -p bar') .
+	print 9 $(a=1 b=2 c=a "$__progname" -c 'typeset -p c; c=b; typeset -p c; integer c; typeset -p c') .
+	print 0 $(a=1 b=2 c=a "$__progname" -c 'typeset -p c;      typeset -p c; integer c; typeset -p c') .
+expected-stdout:
+	1 , .
+	2 123 , set -A bar typeset -x bar[123]=baz .
+	3 0 , .
+	4 123 , set -A bar typeset -x bar[123]=baz .
+	5 8#123 .
+	6 16#123 , .
+	7 12#123 , .
+	8 0 , .
+	9 typeset -x c=a typeset -x c=b typeset -i -x c=2 .
+	0 typeset -x c=a typeset -x c=a typeset -i -x c=0 .
+---
 name: utilities-getopts-1
 description:
 	getopts sets OPTIND correctly for unparsed option
diff -pruN mksh_59c-4/edit.c mksh_59c-6/edit.c
--- mksh_59c-4/edit.c	2021-05-03 00:29:50.000000000 +0200
+++ mksh_59c-6/edit.c	2021-05-03 03:57:57.000000000 +0200
@@ -74,6 +74,7 @@ static struct {
 #define XCF_COMMAND_FILE (XCF_COMMAND | XCF_FILE)
 #define XCF_IS_COMMAND	BIT(3)	/* return flag: is command */
 #define XCF_IS_NOSPACE	BIT(4)	/* return flag: do not append a space */
+#define XCF_IS_HOMEDIR	BIT(5)	/* return flag: tilde needs slash */
 
 static char editmode;
 static int xx_cols;			/* for Emacs mode */
@@ -92,7 +93,7 @@ static int x_do_comment(char *, ssize_t,
 static void x_print_expansions(int, char * const *, bool);
 static int x_cf_glob(int *, const char *, int, int, int *, int *, char ***);
 static size_t x_longest_prefix(int, char * const *);
-static void x_glob_hlp_add_qchar(char *);
+static char *x_glob_hlp_add_qchar(char *);
 static char *x_glob_hlp_tilde_and_rem_qchar(char *, bool);
 static size_t x_basename(const char *, const char *);
 static void x_free_words(int, char **);
@@ -297,21 +298,32 @@ x_print_expansions(int nwords, char * co
 
 /*
  * Convert backslash-escaped string to QCHAR-escaped
- * string useful for globbing; loses QCHAR unless it
- * can squeeze in, eg. by previous loss of backslash
+ * string useful for globbing
  */
-static void
+static char *
 x_glob_hlp_add_qchar(char *cp)
 {
-	char ch, *dp = cp;
+	char ch, *dp;
 	bool escaping = false;
+	XString xs;
+	size_t n;
+
+	if (memchr(cp, QCHAR, (n = strlen(cp)))) {
+		Xinit(xs, dp, n, ATEMP);
+	} else {
+		xs.len = n + 1;
+		xs.areap = NULL; /* won’t be used */
+		xs.beg = dp = cp;
+		xs.end = xs.beg + xs.len;
+	}
 
 	while ((ch = *cp++)) {
 		if (ch == '\\' && !escaping) {
 			escaping = true;
 			continue;
 		}
-		if (escaping || (ch == QCHAR && (cp - dp) > 1)) {
+		XcheckN(xs, dp, 2);
+		if (escaping || ch == QCHAR) {
 			/*
 			 * empirically made list of chars to escape
 			 * for globbing as well as QCHAR itself
@@ -324,6 +336,7 @@ x_glob_hlp_add_qchar(char *cp)
 			case ORD('['):
 			case ORD('\\'):
 			case ORD('`'):
+			case ORD('~'):
 				*dp++ = QCHAR;
 				break;
 			}
@@ -332,6 +345,7 @@ x_glob_hlp_add_qchar(char *cp)
 		*dp++ = ch;
 	}
 	*dp = '\0';
+	return (Xstring(xs, dp));
 }
 
 /*
@@ -367,8 +381,12 @@ x_glob_hlp_tilde_and_rem_qchar(char *s,
 	}
 
 	/* ... convert it from backslash-escaped via QCHAR-escaped... */
-	if (magic_flag)
-		x_glob_hlp_add_qchar(s);
+	if (magic_flag) {
+		cp = x_glob_hlp_add_qchar(s);
+		if (cp != s)
+			afree(s, ATEMP);
+		s = cp;
+	}
 	/* ... to unescaped, for comparison with the matches */
 	cp = dp = s;
 
@@ -391,25 +409,26 @@ x_glob_hlp_tilde_and_rem_qchar(char *s,
 static int
 x_file_glob(int *flagsp, char *toglob, char ***wordsp)
 {
-	char **words, *cp;
+	char **words, *cp, *qglob;
 	int nwords;
 	XPtrV w;
 	struct source *s, *sold;
 
 	/* remove all escaping backward slashes */
-	x_glob_hlp_add_qchar(toglob);
+	qglob = x_glob_hlp_add_qchar(toglob);
 
 	/*
 	 * Convert "foo*" (toglob) to an array of strings (words)
 	 */
 	sold = source;
 	s = pushs(SWSTR, ATEMP);
-	s->start = s->str = toglob;
+	s->start = s->str = qglob;
 	source = s;
 	if (yylex(ONEWORD | LQCHAR) != LWORD) {
 		source = sold;
 		internal_warningf(Tfg_badsubst);
-		return (0);
+		nwords = 0;
+		goto out;
 	}
 	source = sold;
 	afree(s, ATEMP);
@@ -434,7 +453,7 @@ x_file_glob(int *flagsp, char *toglob, c
 		struct stat statb;
 
 		/* Expand any tilde and drop all QCHAR for comparison */
-		toglob = x_glob_hlp_tilde_and_rem_qchar(toglob, false);
+		qglob = x_glob_hlp_tilde_and_rem_qchar(qglob, false);
 
 		/*
 		 * Check if globbing failed (returned glob pattern),
@@ -444,7 +463,7 @@ x_file_glob(int *flagsp, char *toglob, c
 		 * to glob something which evaluated to an empty
 		 * string (e.g., "$FOO" when there is no FOO, etc).
 		 */
-		if ((strcmp(words[0], toglob) == 0 &&
+		if ((strcmp(words[0], qglob) == 0 &&
 		    stat(words[0], &statb) < 0) ||
 		    words[0][0] == '\0') {
 			x_free_words(nwords, words);
@@ -456,6 +475,9 @@ x_file_glob(int *flagsp, char *toglob, c
 	if ((*wordsp = nwords ? words : NULL) == NULL && words != NULL)
 		x_free_words(nwords, words);
 
+ out:
+	if (qglob != toglob)
+		afree(qglob, ATEMP);
 	return (nwords);
 }
 
@@ -671,7 +693,7 @@ x_cf_glob(int *flagsp, const char *buf,
 
 		if (*toglob == '~' && /* not vdirsep */ !vstrchr(toglob, '/')) {
 			/* neither for '~foo' (but '~foo/bar') */
-			*flagsp |= XCF_IS_NOSPACE;
+			*flagsp |= XCF_IS_HOMEDIR;
 			goto dont_add_glob;
 		}
 
@@ -2884,11 +2906,13 @@ do_complete(
 	x_adjust();
 	/*
 	 * append a space if this is a single non-directory match
-	 * and not a parameter or homedir substitution
+	 * and not a parameter substitution, slash for homedir
 	 */
-	if (nwords == 1 && !mksh_cdirsep(words[0][nlen - 1]) &&
-	    !(flags & XCF_IS_NOSPACE)) {
-		x_ins(T1space);
+	if (nwords == 1 && !mksh_cdirsep(words[0][nlen - 1])) {
+		if (flags & XCF_IS_HOMEDIR)
+			x_ins("/");
+		else if (!(flags & XCF_IS_NOSPACE))
+			x_ins(T1space);
 	}
 
 	x_free_words(nwords, words);
@@ -5536,11 +5560,14 @@ complete_word(int cmd, int count)
 
 		/*
 		 * append a space if this is a non-directory match
-		 * and not a parameter or homedir substitution
+		 * and not a parameter substitution, slash for homedir
 		 */
-		if (match_len > 0 && !mksh_cdirsep(match[match_len - 1]) &&
-		    !(flags & XCF_IS_NOSPACE))
-			rval = putbuf(T1space, 1, false);
+		if (match_len > 0 && !mksh_cdirsep(match[match_len - 1])) {
+			if (flags & XCF_IS_HOMEDIR)
+				rval = putbuf("/", 1, false);
+			else if (!(flags & XCF_IS_NOSPACE))
+				rval = putbuf(T1space, 1, false);
+		}
 	}
 	x_free_words(nwords, words);
 
diff -pruN mksh_59c-4/eval.c mksh_59c-6/eval.c
--- mksh_59c-4/eval.c	2020-05-05 23:34:54.000000000 +0200
+++ mksh_59c-6/eval.c	2021-05-03 03:57:57.000000000 +0200
@@ -1300,7 +1300,7 @@ varsub(Expand *xp, const char *sp, const
 			if (sc & 2) {
 				stype = 0;
 				XPinit(wv, 32);
-				vp = global(arrayname(sp));
+				vp = arraybase(sp);
 				do {
 					if (vp->flag & ISSET)
 						XPput(wv, shf_smprintf(Tf_lu,
@@ -1347,7 +1347,7 @@ varsub(Expand *xp, const char *sp, const
 		case ORD('#'):
 			  switch (sc & 3) {
 			case 3:
-				vp = global(arrayname(sp));
+				vp = arraybase(sp);
 				if (vp->flag & (ISSET|ARRAY))
 					zero_ok = true;
 				sc = 0;
@@ -1458,7 +1458,7 @@ varsub(Expand *xp, const char *sp, const
 		/* do what we can */
 		if (sc & 2) {
 			XPinit(wv, 32);
-			vp = global(arrayname(sp));
+			vp = arraybase(sp);
 			do {
 				if (vp->flag & ISSET)
 					XPput(wv, str_val(vp));
diff -pruN mksh_59c-4/misc.c mksh_59c-6/misc.c
--- mksh_59c-4/misc.c	2021-05-03 00:29:50.000000000 +0200
+++ mksh_59c-6/misc.c	2021-05-03 03:57:57.000000000 +0200
@@ -2398,6 +2398,7 @@ chvt(const Getopt *go)
 			errorf(Tf_sD_s_s, "chvt", Tcant_open, dv);
 		}
 	}
+	afree(cp, ATEMP);
 	if (go->optarg[0] != '!') {
 		switch (fork()) {
 		case -1:
diff -pruN mksh_59c-4/mksh.1 mksh_59c-6/mksh.1
--- mksh_59c-4/mksh.1	2021-05-03 00:29:50.000000000 +0200
+++ mksh_59c-6/mksh.1	2021-05-03 03:57:57.000000000 +0200
@@ -84,7 +84,7 @@
 .\" with -mandoc, it might implement .Mx itself, but we want to
 .\" use our own definition. And .Dd must come *first*, always.
 .\"
-.Dd March 13, 2021
+.Dd $Mdocdate: May 2 2021 $
 .\"
 .\" Check which macro package we use, and do other -mdoc setup.
 .\"
@@ -6324,7 +6324,7 @@ replaces the inserted text string with t
 .Pp
 The tab completion escapes characters the same way as the following code:
 .Bd -literal
-print \-nr \-\- "${x@/[\e"\-\e$\e&\-*:\-?[\e\e\e\`\e{\-\e}${IFS\-$\*(aq \et\en\*(aq}]/\e\e$KSH_MATCH}"
+print \-nr \-\- "${x@/[\e"\-\e$\e&\-*:\-?[\e\e\e\`\e{\-\e\*(TI${IFS\-$\*(aq \et\en\*(aq}]/\e\e$KSH_MATCH}"
 .Ed
 .Ss Vi editing mode
 .Em Note:
diff -pruN mksh_59c-4/mksh.faq mksh_59c-6/mksh.faq
--- mksh_59c-4/mksh.faq	2021-05-03 00:29:50.000000000 +0200
+++ mksh_59c-6/mksh.faq	2021-05-03 03:57:57.000000000 +0200
@@ -1,4 +1,4 @@
-RCSID: $MirOS: src/bin/mksh/mksh.faq,v 1.17+locale-tracking 2021/03/11 14:16:08 tg Exp $
+RCSID: $MirOS: src/bin/mksh/mksh.faq,v 1.19+locale-tracking 2021/05/02 08:26:03 tg Exp $
 ToC: spelling
 Title: How do you spell <tt>mksh</tt>? How do you pronounce it?
 
@@ -412,7 +412,7 @@ Title: What about programmable tab compl
 The shell itself provides static deterministic tab completion.
 However, you can use hooks like reprogramming the Tab key to a
 command line editor macro, and using the <tt>evaluate-region</tt>
-editor command (modulo a bugfix) together with <tt>quote-region</tt> and shell functions to
+editor command together with <tt>quote-region</tt> and shell functions to
 implement a programmable completion engine. Multiple people have
 been considering doing so in our IRC channel; we’ll hyperlink to
 these engines when they are available.
@@ -612,6 +612,20 @@ Title: Didn’t there used to be a sleep
  In that case, GNU coreutils’ sleep, which is built on older syscalls,
  may work if the copyleft licence isn’t a showstopper for you.</p>
 ----
+ToC: arith-import
+Title: Some integer variables are 0?
+
+<p class="boxhead">To mitigate potential exploits, variables imported
+ from the environment are not trusted in arithmetic context; that is…</p>
+<div class="boxtext">
+ <pre>
+	foo=1+1 mksh -c 'integer foo; print $foo'
+	foo=1+1 mksh -c 'integer foo=$foo; print $foo'
+ </pre>
+</div><p class="boxfoot">… will lose the value in the first line,
+ while the second line explicitly “untaints”, to use a Perl term,
+ the content. Purely numeric values will pass, though.</p>
+----
 ToC: string-concat
 Title: “+=” behaves differently from other shells
 
diff -pruN mksh_59c-4/sh.h mksh_59c-6/sh.h
--- mksh_59c-4/sh.h	2021-05-03 00:29:50.000000000 +0200
+++ mksh_59c-6/sh.h	2021-05-03 03:57:57.000000000 +0200
@@ -195,7 +195,7 @@
 #ifdef EXTERN
 __RCSID("$MirOS: src/bin/mksh/sh.h,v 1.906 2021/01/24 19:37:31 tg Exp $");
 #endif
-#define MKSH_VERSION "R59 2021/03/13"
+#define MKSH_VERSION "R59 2021/05/03"
 
 /* arithmetic types: C implementation */
 #if !HAVE_CAN_INTTYPES
@@ -1411,9 +1411,9 @@ EXTERN bool really_exit;
 #define CiOCTAL	BIT(5)	/* 0‥7				*/
 #define CiQCL	BIT(6)	/* &();|			*/
 #define CiALIAS	BIT(7)	/* !,.@				*/
-#define CiQCX	BIT(8)	/* *[\\				*/
+#define CiQCX	BIT(8)	/* *[\\~			*/
 #define CiVAR1	BIT(9)	/* !*@				*/
-#define CiQCM	BIT(10)	/* /^~				*/
+#define CiQCM	BIT(10)	/* /^				*/
 #define CiDIGIT	BIT(11)	/* 89				*/
 #define CiQC	BIT(12)	/* "'				*/
 #define CiSPX	BIT(13)	/* \x0B\x0C			*/
@@ -1470,7 +1470,7 @@ EXTERN char ifs0;
 #define C_EDCMD	(CiGRAVE | CiQCL)
 /* \x09\x0A\x20"&'():;<=>`|	editor non-word characters */
 #define C_EDNWC	(CiANGLE | CiCOLON | CiEQUAL | CiGRAVE | CiNL | CiQC | CiQCL | CiSP | CiTAB)
-/* "#$&'()*:;<=>?[\\`{|}	editor quotes for tab completion */
+/* "#$&'()*:;<=>?[\\`{|}~	editor quotes for tab completion */
 #define C_EDQ	(CiANGLE | CiCOLON | CiCURLY | CiEQUAL | CiGRAVE | CiHASH | CiQC | CiQCL | CiQCX | CiQUEST | CiSS)
 /* !‥~			POSIX graphical (alphanumerical plus punctuation) */
 #define C_GRAPH	(C_PUNCT | CiDIGIT | CiLOWER | CiOCTAL | CiUPPER)
@@ -1494,8 +1494,8 @@ EXTERN char ifs0;
 #define C_PRINT	(C_GRAPH | CiSP)
 /* !"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~	POSIX punctuation */
 #define C_PUNCT	(CiALIAS | CiANGLE | CiBRACK | CiCOLON | CiCURLY | CiEQUAL | CiGRAVE | CiHASH | CiMINUS | CiPERCT | CiPLUS | CiQC | CiQCL | CiQCM | CiQCX | CiQUEST | CiSS | CiUNDER)
-/* \x09\x0A"#$&'()*;<=>?[\\]`|	characters requiring quoting, minus space */
-#define C_QUOTE	(CiANGLE | CiBRACK | CiEQUAL | CiGRAVE | CiHASH | CiNL | CiQC | CiQCL | CiQCX | CiQUEST | CiSS | CiTAB)
+/* \x09\x0A"#$&'()*;<=>?[\\]`{|}~	characters requiring quoting, minus space */
+#define C_QUOTE	(CiANGLE | CiBRACK | CiCURLY | CiEQUAL | CiGRAVE | CiHASH | CiNL | CiQC | CiQCL | CiQCX | CiQUEST | CiSS | CiTAB)
 /* 0‥9A‥Fa‥f		hexadecimal digit */
 #define C_SEDEC	(CiDIGIT | CiHEXLT | CiOCTAL)
 /* \x09‥\x0D\x20	POSIX space class */
@@ -2749,7 +2749,7 @@ struct tbl *arraysearch(struct tbl *, ui
 char **makenv(void);
 void change_winsz(void);
 size_t array_ref_len(const char *) MKSH_A_PURE;
-char *arrayname(const char *);
+struct tbl *arraybase(const char *);
 mksh_uari_t set_array(const char *, bool, const char **);
 uint32_t hash(const void *) MKSH_A_PURE;
 uint32_t chvt_rndsetup(const void *, size_t) MKSH_A_PURE;
diff -pruN mksh_59c-4/shf.c mksh_59c-6/shf.c
--- mksh_59c-4/shf.c	2020-06-22 19:11:30.000000000 +0200
+++ mksh_59c-6/shf.c	2021-05-03 03:57:57.000000000 +0200
@@ -1206,7 +1206,7 @@ const uint32_t tpl_ctypes[128] = {
 	CiLOWER,	CiLOWER,	CiLOWER,	CiLOWER,
 	CiLOWER,	CiLOWER,	CiLOWER,	CiLOWER,
 	CiLOWER,	CiLOWER,	CiLOWER,	CiCURLY,
-	CiQCL,		CiCURLY,	CiQCM,		CiCNTRL
+	CiQCL,		CiCURLY,	CiQCX,		CiCNTRL
 };
 
 void
diff -pruN mksh_59c-4/syn.c mksh_59c-6/syn.c
--- mksh_59c-4/syn.c	2020-10-31 02:22:25.000000000 +0100
+++ mksh_59c-6/syn.c	2021-05-03 03:57:57.000000000 +0200
@@ -74,8 +74,8 @@ static int symbol;			/* yylex value */
 
 #define REJECT		(reject = true)
 #define ACCEPT		(reject = false)
-#define token(cf)	((reject) ? (ACCEPT, symbol) : (symbol = yylex(cf)))
-#define tpeek(cf)	((reject) ? (symbol) : (REJECT, symbol = yylex(cf)))
+#define token(cf)	((reject ? 0 : (symbol = yylex(cf))), ACCEPT, symbol)
+#define tpeek(cf)	((reject ? 0 : (symbol = yylex(cf))), REJECT, symbol)
 #define musthave(c,cf)	do { 					\
 	if ((unsigned int)token(cf) != (unsigned int)(c))	\
 		syntaxerr(NULL);				\
diff -pruN mksh_59c-4/var.c mksh_59c-6/var.c
--- mksh_59c-4/var.c	2020-06-22 19:11:30.000000000 +0200
+++ mksh_59c-6/var.c	2021-05-03 03:57:57.000000000 +0200
@@ -3,7 +3,7 @@
 /*-
  * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
  *		 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018,
- *		 2019
+ *		 2019, 2021
  *	mirabilos <m@mirbsd.org>
  *
  * Provided that these terms and disclaimer and all copyright notices
@@ -29,7 +29,7 @@
 #include <sys/sysctl.h>
 #endif
 
-__RCSID("$MirOS: src/bin/mksh/var.c,v 1.237 2020/06/22 17:11:03 tg Exp $");
+__RCSID("$MirOS: src/bin/mksh/var.c,v 1.237+deb 2020/06/22 17:11:03 tg Exp $");
 
 /*-
  * Variables
@@ -57,6 +57,7 @@ static void getspec(struct tbl *);
 static void setspec(struct tbl *);
 static void unsetspec(struct tbl *, bool);
 static int getint(struct tbl *, mksh_ari_u *, bool);
+static int getnum(const char *, mksh_ari_u *, bool, bool);
 static const char *array_index_calc(const char *, bool *, uint32_t *);
 static struct tbl *vtypeset(int *, const char *, uint32_t, uint32_t, int, int);
 
@@ -494,6 +495,7 @@ setstr(struct tbl *vq, const char *s, in
 			vq->flag |= ALLOC;
 			vq->type = 0;
 		}
+		vq->flag &= ~IMPORT;
 		afree(salloc, ATEMP);
 	} else {
 		/* integer dest */
@@ -527,10 +529,6 @@ setint(struct tbl *vq, mksh_ari_t n)
 static int
 getint(struct tbl *vp, mksh_ari_u *nump, bool arith)
 {
-	mksh_uari_t c, num = 0, base = 10;
-	const char *s;
-	bool have_base = false, neg = false;
-
 	if (vp->flag & SPECIAL)
 		getspec(vp);
 	/* XXX is it possible for ISSET to be set and val.s to be NULL? */
@@ -540,7 +538,15 @@ getint(struct tbl *vp, mksh_ari_u *nump,
 		nump->i = vp->val.i;
 		return (vp->type);
 	}
-	s = vp->val.s + vp->type;
+	return (getnum(vp->val.s + vp->type, nump, arith,
+	    Flag(FPOSIX) && !(vp->flag & ZEROFIL)));
+}
+
+static int
+getnum(const char *s, mksh_ari_u *nump, bool arith, bool psxoctal)
+{
+	mksh_uari_t c, num = 0, base = 10;
+	bool have_base = false, neg = false;
 
 	do {
 		c = (unsigned char)*s++;
@@ -561,8 +567,7 @@ getint(struct tbl *vp, mksh_ari_u *nump,
 			base = 16;
 			++s;
 			goto getint_c_style_base;
-		} else if (Flag(FPOSIX) && ctype(s[0], C_DIGIT) &&
-		    !(vp->flag & ZEROFIL)) {
+		} else if (psxoctal && ctype(s[0], C_DIGIT)) {
 			/* interpret as octal (deprecated) */
 			base = 8;
  getint_c_style_base:
@@ -641,10 +646,11 @@ setint_v(struct tbl *vq, struct tbl *vp,
 void
 setint_n(struct tbl *vq, mksh_ari_t num, int newbase)
 {
-	if (!(vq->flag & INTEGER) && (vq->flag & ALLOC)) {
-		vq->flag &= ~ALLOC;
+	if (!(vq->flag & INTEGER)) {
+		if (vq->flag & ALLOC)
+			afree(vq->val.s, vq->areap);
+		vq->flag &= ~(ALLOC | IMPORT);
 		vq->type = 0;
-		afree(vq->val.s, vq->areap);
 	}
 	vq->val.i = num;
 	if (newbase != 0)
@@ -938,7 +944,7 @@ vtypeset(int *ep, const char *var, uint3
 
 	set &= ~(LOCAL|LOCAL_COPY);
 
-	vpbase = (vp->flag & ARRAY) ? global(arrayname(tvar)) : vp;
+	vpbase = (vp->flag & ARRAY) ? arraybase(tvar) : vp;
 
 	/*
 	 * only allow export and readonly flag to be set; AT&T ksh
@@ -961,7 +967,7 @@ vtypeset(int *ep, const char *var, uint3
 		 */
 		for (t = vpbase; t; t = t->u.array) {
 			bool fake_assign;
-			char *s = NULL;
+			const char *s = NULL;
 			char *free_me = NULL;
 
 			fake_assign = (t->flag & ISSET) && (!val || t != vp) &&
@@ -983,13 +989,27 @@ vtypeset(int *ep, const char *var, uint3
 				t->type = 0;
 				t->flag &= ~ALLOC;
 			}
-			t->flag = (t->flag | set) & ~clr;
 			/*
 			 * Don't change base if assignment is to be
 			 * done, in case assignment fails.
 			 */
-			if ((set & INTEGER) && base > 0 && (!val || t != vp))
-				t->type = base;
+			if (set & INTEGER) {
+				if (base > 0 && (!val || t != vp))
+					t->type = base;
+				/*
+				 * Do not permit content from the
+				 * environment to e.g. execute commands.
+				 */
+				if ((t->flag & IMPORT) && fake_assign) {
+					mksh_ari_u num;
+
+					if (getnum(s, &num, true,
+					    tobool(Flag(FPOSIX))) == -1)
+						s = "0";
+					clr |= IMPORT;
+				}
+			}
+			t->flag = (t->flag | set) & ~clr;
 			if (set & (LJUST|RJUST|ZEROFIL))
 				t->u2.field = field;
 			if (fake_assign) {
@@ -1019,7 +1039,7 @@ vtypeset(int *ep, const char *var, uint3
 
 	if (vappend) {
 		size_t tlen;
-		if ((vp->flag & (ISSET|ALLOC|SPECIAL|INTEGER|UCASEV_AL|LCASEV|LJUST|RJUST)) != (ISSET|ALLOC)) {
+		if ((vp->flag & (ISSET|ALLOC|SPECIAL|INTEGER|UCASEV_AL|LCASEV|LJUST|RJUST|IMPORT)) != (ISSET|ALLOC)) {
 			/* cannot special-case this */
 			strdup2x(tvar, str_val(vp), val);
 			val = tvar;
@@ -1038,9 +1058,11 @@ vtypeset(int *ep, const char *var, uint3
 			/* done after assignment to override default */
 			if (base > 0)
 				vp->type = base;
-		} else
+		} else {
 			/* setstr can't fail (readonly check already done) */
 			setstr(vp, val, KSH_RETURN_ERROR | 0x4);
+			vp->flag |= (set & IMPORT);
+		}
 
 		/* came here from vappend? need to free temp val */
 		if (vappend)
@@ -1610,19 +1632,25 @@ array_ref_len(const char *cp)
 }
 
 /*
- * Make a copy of the base of an array name
+ * same effect as global(copy of the base of an array name)
  */
-char *
-arrayname(const char *str)
+struct tbl *
+arraybase(const char *str)
 {
 	const char *p;
-	char *rv;
-
-	if (!(p = cstrchr(str, '[')))
-		/* Shouldn't happen, but why worry? */
-		strdupx(rv, str, ATEMP);
-	else
-		strndupx(rv, str, p - str, ATEMP);
+	char *s, sbuf[32];
+	size_t n;
+	struct tbl *rv;
+
+	n = strlen(str);
+	if ((p = memchr(str, '[', n)))
+		n = p - str;
+	s = n < sizeof(sbuf) ? sbuf : alloc(n + 1, ATEMP);
+	memcpy(s, str, n);
+	s[n] = '\0';
+	rv = global(s);
+	if (s != sbuf)
+		afree(s, ATEMP);
 
 	return (rv);
 }

Reply to: