Module Name:    src
Committed By:   kre
Date:           Wed Apr 10 08:13:11 UTC 2019

Modified Files:
        src/bin/sh: expand.c
        src/tests/bin/sh: t_expand.sh

Log Message:
PR bin/54112

Fix handling of "$@" (that is, double quoted dollar at), when it
appears in a string which will be subject to field splitting.

Eg:
        ${0+"$@" }

More common usages, like the simple "$@" or ${0+"$@"} end up
being entirely quoted, so no field splitting happens, and the
problem was avoided.

See the PR for more details.

This ends up making a bunch of old hack code (and some that was
relatively new) vanish - for now it is just #if 0'd or commented out.
Cleanups of that stuff will happen later.

That some of the worst $@ hacks are now gone does not mean that processing
of "$@" does not retain a very special place in every hackers heart.
RIP extreme ugliness - long live the merely ordinary ugly.

Added a new bin/sh ATF test case to verify that all this remains fixed.


To generate a diff of this commit:
cvs rdiff -u -r1.131 -r1.132 src/bin/sh/expand.c
cvs rdiff -u -r1.20 -r1.21 src/tests/bin/sh/t_expand.sh

Please note that diffs are not public domain; they are subject to the
copyright notices on the relevant files.

Modified files:

Index: src/bin/sh/expand.c
diff -u src/bin/sh/expand.c:1.131 src/bin/sh/expand.c:1.132
--- src/bin/sh/expand.c:1.131	Wed Feb 27 04:10:56 2019
+++ src/bin/sh/expand.c	Wed Apr 10 08:13:11 2019
@@ -1,4 +1,4 @@
-/*	$NetBSD: expand.c,v 1.131 2019/02/27 04:10:56 kre Exp $	*/
+/*	$NetBSD: expand.c,v 1.132 2019/04/10 08:13:11 kre Exp $	*/
 
 /*-
  * Copyright (c) 1991, 1993
@@ -37,7 +37,7 @@
 #if 0
 static char sccsid[] = "@(#)expand.c	8.5 (Berkeley) 5/15/95";
 #else
-__RCSID("$NetBSD: expand.c,v 1.131 2019/02/27 04:10:56 kre Exp $");
+__RCSID("$NetBSD: expand.c,v 1.132 2019/04/10 08:13:11 kre Exp $");
 #endif
 #endif /* not lint */
 
@@ -97,6 +97,8 @@ struct ifsregion ifsfirst;	/* first stru
 struct ifsregion *ifslastp;	/* last struct in list */
 struct arglist exparg;		/* holds expanded arg list */
 
+static int empty_dollar_at;	/* have expanded "$@" to nothing */
+
 STATIC const char *argstr(const char *, int);
 STATIC const char *exptilde(const char *, int);
 STATIC void expbackq(union node *, int, int);
@@ -180,6 +182,7 @@ expandarg(union node *arg, struct arglis
 	if (fflag)		/* no filename expandsion */
 		flag &= ~EXP_GLOB;
 
+	empty_dollar_at = 0;
 	argbackq = arg->narg.backquote;
 	STARTSTACKSTR(expdest);
 	ifsfirst.next = NULL;
@@ -243,6 +246,8 @@ argstr(const char *p, int flag)
 	char c;
 	const int quotes = flag & EXP_QNEEDED;		/* do CTLESC */
 	int firsteq = 1;
+	int had_dol_at = 0;
+	int startoff;
 	const char *ifs = NULL;
 	int ifs_split = EXP_IFS_SPLIT;
 
@@ -251,6 +256,7 @@ argstr(const char *p, int flag)
 
 	CTRACE(DBG_EXPAND, ("argstr(\"%s\", %#x) quotes=%#x\n", p,flag,quotes));
 
+	startoff = expdest - stackblock();
 	if (*p == '~' && (flag & (EXP_TILDE | EXP_VARTILDE)))
 		p = exptilde(p, flag);
 	for (;;) {
@@ -262,6 +268,8 @@ argstr(const char *p, int flag)
 			return p - 1;
 		case CTLENDVAR: /* end of expanding yyy in ${xxx-yyy} */
 		case CTLENDARI: /* end of a $(( )) string */
+			if (had_dol_at && (*p&0xFF) == CTLQUOTEEND)
+				p++;
 			NULLTERM_4_TRACE(expdest);
 			VTRACE(DBG_EXPAND, ("argstr returning at \"%.6s\"..."
 			    " after %2.2X; added \"%s\" to expdest\n",
@@ -270,8 +278,12 @@ argstr(const char *p, int flag)
 		case CTLQUOTEMARK:
 			/* "$@" syntax adherence hack */
 			if (p[0] == CTLVAR && p[1] & VSQUOTE &&
-			    p[2] == '@' && p[3] == '=')
+			    p[2] == '@' && p[3] == '=') {
+				had_dol_at = 1;
 				break;
+			}
+			had_dol_at = 0;
+			empty_dollar_at = 0;
 			if ((flag & EXP_SPLIT) != 0)
 				STPUTC(c, expdest);
 			ifs_split = 0;
@@ -285,9 +297,14 @@ argstr(const char *p, int flag)
 			STPUTC('\n', expdest);	/* no line_number++ */
 			break;
 		case CTLQUOTEEND:
-			if ((flag & EXP_SPLIT) != 0)
+			if (empty_dollar_at &&
+			    expdest - stackblock() > startoff &&
+			    expdest[-1] == CTLQUOTEMARK)
+				expdest--;
+			else if (!had_dol_at && (flag & EXP_SPLIT) != 0)
 				STPUTC(c, expdest);
 			ifs_split = EXP_IFS_SPLIT;
+			had_dol_at = 0;
 			break;
 		case CTLESC:
 			if (quotes || ISCTL(*p))
@@ -890,6 +907,8 @@ evalvar(const char *p, int flag)
 	} else if (special) {
 		set = varisset(var, varflags & VSNUL);
 		val = NULL;
+		if (!set && *var == '@')
+			empty_dollar_at = 1;
 	} else {
 		val = lookupvar(var);
 		if (val == NULL || ((varflags & VSNUL) && val[0] == '\0')) {
@@ -916,9 +935,11 @@ evalvar(const char *p, int flag)
 		}
 	}
 
+#if 0		/* no longer need this $@ evil ... */
 	if (!set && subtype != VSPLUS && special && *var == '@')
 		if (startloc > 0 && expdest[-1] == CTLQUOTEMARK)
 			expdest--, startloc--;
+#endif
 
 	if (set && subtype != VSPLUS) {
 		/* insert the value of the variable */
@@ -1202,13 +1223,23 @@ varvalue(const char *name, int quoted, i
 		if (flag & EXP_SPLIT && quoted) {
 			VTRACE(DBG_EXPAND, (": $@ split (%d)\n",
 			    shellparam.nparam));
+#if 0
 		/* GROSS HACK */
 			if (shellparam.nparam == 0 &&
 				expdest[-1] == CTLQUOTEMARK)
 					expdest--;
 		/* KCAH SSORG */
+#endif
+			if (shellparam.nparam == 0)
+				empty_dollar_at = 1;
+
 			for (ap = shellparam.p ; (p = *ap++) != NULL ; ) {
-				STRTODEST(p);
+				if (*p == '\0') {
+					/* retain an explicit null string */
+					STPUTC(CTLQUOTEMARK, expdest);
+					STPUTC(CTLQUOTEEND, expdest);
+				} else
+					STRTODEST(p);
 				if (*ap)
 					/* A NUL separates args inside "" */
 					STPUTC('\0', expdest);
@@ -1396,8 +1427,10 @@ ifsbreakup(char *string, struct arglist 
 		}
 	}
 
+/*
 	while (*start == CTLQUOTEEND)
 		start++;
+*/
 
 	/*
 	 * Save anything left as an argument.

Index: src/tests/bin/sh/t_expand.sh
diff -u src/tests/bin/sh/t_expand.sh:1.20 src/tests/bin/sh/t_expand.sh:1.21
--- src/tests/bin/sh/t_expand.sh:1.20	Tue Nov 27 09:59:30 2018
+++ src/tests/bin/sh/t_expand.sh	Wed Apr 10 08:13:11 2019
@@ -1,4 +1,4 @@
-# $NetBSD: t_expand.sh,v 1.20 2018/11/27 09:59:30 kre Exp $
+# $NetBSD: t_expand.sh,v 1.21 2019/04/10 08:13:11 kre Exp $
 #
 # Copyright (c) 2007, 2009 The NetBSD Foundation, Inc.
 # All rights reserved.
@@ -46,7 +46,7 @@ delim_argv() {
 
 atf_test_case dollar_at
 dollar_at_head() {
-	atf_set "descr" "Somewhere between 2.0.2 and 3.0 the expansion" \
+	atf_set descr "Somewhere between 2.0.2 and 3.0 the expansion" \
 	                "of the \$@ variable had been broken.  Check for" \
 			"this behavior."
 }
@@ -65,7 +65,7 @@ dollar_at_body() {
 
 atf_test_case dollar_at_unquoted_or_conditional
 dollar_at_unquoted_or_conditional_head() {
-	atf_set "descr" 'Sometime during 2013 the expansion of "${1+$@}"' \
+	atf_set descr 'Sometime during 2013 the expansion of "${1+$@}"' \
 			' (where $1 and $2 (and maybe more) are set)' \
 			' seems to have broken.  Check for this bug.'
 }
@@ -87,7 +87,7 @@ dollar_at_unquoted_or_conditional_body()
 
 atf_test_case dollar_at_with_text
 dollar_at_with_text_head() {
-	atf_set "descr" "Test \$@ expansion when it is surrounded by text" \
+	atf_set descr "Test \$@ expansion when it is surrounded by text" \
 	                "within the quotes.  PR bin/33956."
 }
 dollar_at_with_text_body() {
@@ -160,7 +160,7 @@ EOF
 
 atf_test_case dollar_at_empty_and_conditional
 dollar_at_empty_and_conditional_head() {
-	atf_set "descr" 'Test $@ expansion when there are no args, and ' \
+	atf_set descr 'Test $@ expansion when there are no args, and ' \
 	                'when conditionally expanded.'
 }
 dollar_at_empty_and_conditional_body() {
@@ -355,7 +355,7 @@ EOF
 
 atf_test_case strip
 strip_head() {
-	atf_set "descr" "Checks that the %% operator works and strips" \
+	atf_set descr "Checks that the %% operator works and strips" \
 	                "the contents of a variable from the given point" \
 			"to the end"
 }
@@ -385,7 +385,7 @@ strip_body() {
 
 atf_test_case wrap_strip
 wrap_strip_head() {
-	atf_set "descr" "Checks that the %% operator works and strips" \
+	atf_set descr "Checks that the %% operator works and strips" \
 	                "the contents of a variable from the given point" \
 			'to the end, and that \ \n sequences do not break it'
 }
@@ -429,7 +429,7 @@ ne%\
 
 atf_test_case tilde
 tilde_head() {
-	atf_set "descr" "Checks that the ~ expansions work"
+	atf_set descr "Checks that the ~ expansions work"
 }
 tilde_body() {
 	for HOME in '' / /home/foo \
@@ -489,7 +489,7 @@ ${U4:=~/:~/}\t'"${HOME}"'/:'"${HOME}"'/\
 
 atf_test_case varpattern_backslashes
 varpattern_backslashes_head() {
-	atf_set "descr" "Tests that protecting wildcards with backslashes" \
+	atf_set descr "Tests that protecting wildcards with backslashes" \
 	                "works in variable patterns."
 }
 varpattern_backslashes_body() {
@@ -501,7 +501,7 @@ varpattern_backslashes_body() {
 
 atf_test_case arithmetic
 arithmetic_head() {
-	atf_set "descr" "POSIX requires shell arithmetic to use signed" \
+	atf_set descr "POSIX requires shell arithmetic to use signed" \
 	                "long or a wider type.  We use intmax_t, so at" \
 			"least 64 bits should be available.  Make sure" \
 			"this is true."
@@ -518,7 +518,7 @@ arithmetic_body() {
 
 atf_test_case iteration_on_null_parameter
 iteration_on_null_parameter_head() {
-	atf_set "descr" "Check iteration of \$@ in for loop when set to null;" \
+	atf_set descr "Check iteration of \$@ in for loop when set to null;" \
 	                "the error \"sh: @: parameter not set\" is incorrect." \
 	                "PR bin/48202."
 }
@@ -529,7 +529,7 @@ iteration_on_null_parameter_body() {
 
 atf_test_case iteration_on_quoted_null_parameter
 iteration_on_quoted_null_parameter_head() {
-	atf_set "descr" \
+	atf_set descr \
 		'Check iteration of "$@" in for loop when set to null;'
 }
 iteration_on_quoted_null_parameter_body() {
@@ -539,7 +539,7 @@ iteration_on_quoted_null_parameter_body(
 
 atf_test_case iteration_on_null_or_null_parameter
 iteration_on_null_or_null_parameter_head() {
-	atf_set "descr" \
+	atf_set descr \
 		'Check expansion of null parameter as default for another null'
 }
 iteration_on_null_or_null_parameter_body() {
@@ -549,7 +549,7 @@ iteration_on_null_or_null_parameter_body
 
 atf_test_case iteration_on_null_or_missing_parameter
 iteration_on_null_or_missing_parameter_head() {
-	atf_set "descr" \
+	atf_set descr \
 	    'Check expansion of missing parameter as default for another null'
 }
 iteration_on_null_or_missing_parameter_body() {
@@ -666,7 +666,7 @@ results()
 
 atf_test_case shell_params
 shell_params_head() {
-	atf_set "descr" "Test correct operation of the numeric parameters"
+	atf_set descr "Test correct operation of the numeric parameters"
 }
 shell_params_body() {
 	atf_require_prog mktemp
@@ -725,7 +725,7 @@ shell_params_body() {
 
 atf_test_case var_with_embedded_cmdsub
 var_with_embedded_cmdsub_head() {
-	atf_set "descr" "Test expansion of vars with embedded cmdsub"
+	atf_set descr "Test expansion of vars with embedded cmdsub"
 }
 var_with_embedded_cmdsub_body() {
 
@@ -782,7 +782,7 @@ var_with_embedded_cmdsub_body() {
 
 atf_test_case dollar_hash
 dollar_hash_head() {
-	atf_set "descr" 'Test expansion of various aspects of $#'
+	atf_set descr 'Test expansion of various aspects of $#'
 }
 dollar_hash_body() {
 
@@ -994,7 +994,7 @@ dollar_hash_body() {
 
 atf_test_case dollar_star
 dollar_star_head() {
-	atf_set "descr" 'Test expansion of various aspects of $*'
+	atf_set descr 'Test expansion of various aspects of $*'
 }
 dollar_star_body() {
 
@@ -1038,7 +1038,7 @@ dollar_star_body() {
 
 atf_test_case dollar_star_in_word
 dollar_star_in_word_head() {
-	atf_set "descr" 'Test expansion $* occurring in word of ${var:-word}'
+	atf_set descr 'Test expansion $* occurring in word of ${var:-word}'
 }
 dollar_star_in_word_body() {
 
@@ -1090,7 +1090,7 @@ dollar_star_in_word_body() {
 
 atf_test_case dollar_star_with_empty_ifs
 dollar_star_with_empty_ifs_head() {
-	atf_set "descr" 'Test expansion of $* with IFS=""'
+	atf_set descr 'Test expansion of $* with IFS=""'
 }
 dollar_star_with_empty_ifs_body() {
 
@@ -1124,7 +1124,7 @@ dollar_star_with_empty_ifs_body() {
 
 atf_test_case dollar_star_in_word_empty_ifs
 dollar_star_in_word_empty_ifs_head() {
-	atf_set "descr" 'Test expansion of ${unset:-$*} with IFS=""'
+	atf_set descr 'Test expansion of ${unset:-$*} with IFS=""'
 }
 dollar_star_in_word_empty_ifs_body() {
 
@@ -1166,7 +1166,7 @@ dollar_star_in_word_empty_ifs_body() {
 
 atf_test_case dollar_star_in_quoted_word
 dollar_star_in_quoted_word_head() {
-	atf_set "descr" 'Test expansion $* occurring in word of ${var:-"word"}'
+	atf_set descr 'Test expansion $* occurring in word of ${var:-"word"}'
 }
 dollar_star_in_quoted_word_body() {
 
@@ -1224,9 +1224,65 @@ dollar_star_in_quoted_word_body() {
 	results	  # FIXED: 'PR bin/52090 - 2 of 26 subtests expected to fail'
 }
 
+atf_test_case dollar_at_in_field_split_context
+dollar_at_in_field_split_context_head() {
+	atf_set descr 'Test "$@" wth field splitting -- PR bin/54112'
+}
+dollar_at_in_field_split_context_body() {
+	reset dollar_at_in_field_split_context
+
+		# the simple case (no field split) which always worked
+	check 'set -- ""; set -- ${0+"$@"}; echo $#'		1	0   #1
+
+		# The original failure case from the bash-bug list
+	check 'set -- ""; set -- ${0+"$@" "$@"}; echo $#'	2	0   #2
+
+		# slightly simpler cases that triggered the same issue
+	check 'set -- ""; set -- ${0+"$@" }; echo $#'		1	0   #3
+	check 'set -- ""; set -- ${0+ "$@"}; echo $#'		1	0   #4
+	check 'set -- ""; set -- ${0+ "$@" }; echo $#'		1	0   #5
+
+		# and the bizarre
+	check 'set -- ""; set -- ${0+"$@" "$@" "$@"}; echo $#'	3	0   #6
+
+	# repeat tests when there is more than one set empty numeric param
+
+	check 'set -- "" ""; set -- ${0+"$@"}; echo $#'		2	0   #7
+	check 'set -- "" ""; set -- ${0+"$@" "$@"}; echo $#'	4	0   #8
+	check 'set -- "" ""; set -- ${0+"$@" }; echo $#'	2	0   #9
+	check 'set -- "" ""; set -- ${0+ "$@"}; echo $#'	2	0   #10
+	check 'set -- "" ""; set -- ${0+ "$@" }; echo $#'	2	0   #11
+	check 'set -- "" ""; set -- ${0+"$@" "$@" "$@"}; echo $#' \
+								6	0   #12
+
+		# Next some checks of the way the NetBSD shell
+		# interprets some expressions that are POSIX unspecified.
+		# Other shells might fail these tests, without that
+		# being a problem.   We retain these tests so accidental
+		# changes in our behaviour can be detected.
+
+	check 'set --; X=; set -- "$X$@"; echo $#'		0	0   #13
+	check 'set --; X=; set -- "$@$X"; echo $#'		0	0   #14
+	check 'set --; X=; set -- "$X$@$X"; echo $#'		0	0   #15
+	check 'set --; X=; set -- "$@$@"; echo $#'		0	0   #16
+
+	check 'set -- ""; X=; set -- "$X$@"; echo $#'		1	0   #17
+	check 'set -- ""; X=; set -- "$@$X"; echo $#'		1	0   #19
+	check 'set -- ""; X=; set -- "$X$@$X"; echo $#'		1	0   #19
+	check 'set -- ""; X=; set -- "$@$@"; echo $#'		1	0   #20
+
+	check 'set -- "" ""; X=; set -- "$X$@"; echo $#'	2	0   #21
+	check 'set -- "" ""; X=; set -- "$@$X"; echo $#'	2	0   #22
+	check 'set -- "" ""; X=; set -- "$X$@$X"; echo $#'	2	0   #23
+		# Yes, this next one really is (and should be) 3...
+	check 'set -- "" ""; X=; set -- "$@$@"; echo $#'	3	0   #24
+
+	results
+}
+
 atf_test_case embedded_nl
 embedded_nl_head() {
-	atf_set "descr" 'Test literal \n in xxx string in ${var-xxx}'
+	atf_set descr 'Test literal \n in xxx string in ${var-xxx}'
 }
 embedded_nl_body() {
 
@@ -1281,9 +1337,10 @@ atf_init_test_cases() {
 
 	atf_add_test_case arithmetic
 	atf_add_test_case dollar_at
+	atf_add_test_case dollar_at_empty_and_conditional
+	atf_add_test_case dollar_at_in_field_split_context
 	atf_add_test_case dollar_at_unquoted_or_conditional
 	atf_add_test_case dollar_at_with_text
-	atf_add_test_case dollar_at_empty_and_conditional
 	atf_add_test_case dollar_hash
 	atf_add_test_case dollar_star
 	atf_add_test_case dollar_star_in_quoted_word

Reply via email to