xref: /freebsd/contrib/bmake/unit-tests/varmod-loop.mk (revision 2eb4d8dc723da3cf7d735a3226ae49da4c8c5dbc)
1# $NetBSD: varmod-loop.mk,v 1.15 2021/04/11 13:35:56 rillig Exp $
2#
3# Tests for the :@var@...${var}...@ variable modifier.
4
5.MAKE.SAVE_DOLLARS=	yes
6
7all: varname-overwriting-target
8all: mod-loop-dollar
9
10varname-overwriting-target:
11	# Even "@" works as a variable name since the variable is installed
12	# in the "current" scope, which in this case is the one from the
13	# target.  Because of this, after the loop has finished, '$@' is
14	# undefined.  This is something that make doesn't expect, this may
15	# even trigger an assertion failure somewhere.
16	@echo :$@: :${:U1 2 3:@\@@x${@}y@}: :$@:
17
18
19
20# Demonstrate that it is possible to generate dollar signs using the
21# :@ modifier.
22#
23# These are edge cases that could have resulted in a parse error as well
24# since the $@ at the end could have been interpreted as a variable, which
25# would mean a missing closing @ delimiter.
26mod-loop-dollar:
27	@echo $@:${:U1:@word@${word}$@:Q}:
28	@echo $@:${:U2:@word@$${word}$$@:Q}:
29	@echo $@:${:U3:@word@$$${word}$$$@:Q}:
30	@echo $@:${:U4:@word@$$$${word}$$$$@:Q}:
31	@echo $@:${:U5:@word@$$$$${word}$$$$$@:Q}:
32	@echo $@:${:U6:@word@$$$$$${word}$$$$$$@:Q}:
33
34# It may happen that there are nested :@ modifiers that use the same name for
35# for the loop variable.  These modifiers influence each other.
36#
37# As of 2020-10-18, the :@ modifier is implemented by actually setting a
38# variable in the scope of the expression and deleting it again after the
39# loop.  This is different from the .for loops, which substitute the variable
40# expression with ${:Uvalue}, leading to different unwanted side effects.
41#
42# To make the behavior more predictable, the :@ modifier should restore the
43# loop variable to the value it had before the loop.  This would result in
44# the string "1a b c1 2a b c2 3a b c3", making the two loops independent.
45.if ${:U1 2 3:@i@$i${:Ua b c:@i@$i@}${i:Uu}@} != "1a b cu 2a b cu 3a b cu"
46.  error
47.endif
48
49# During the loop, the variable is actually defined and nonempty.
50# If the loop were implemented in the same way as the .for loop, the variable
51# would be neither defined nor nonempty since all expressions of the form
52# ${var} would have been replaced with ${:Uword} before evaluating them.
53.if defined(var)
54.  error
55.endif
56.if ${:Uword:@var@${defined(var):?def:undef} ${empty(var):?empty:nonempty}@} \
57    != "def nonempty"
58.  error
59.endif
60.if defined(var)
61.  error
62.endif
63
64# Assignment using the ':=' operator, combined with the :@var@ modifier
65#
668_DOLLARS=	$$$$$$$$
67# This string literal is written with 8 dollars, and this is saved as the
68# variable value.  But as soon as this value is evaluated, it goes through
69# Var_Subst, which replaces each '$$' with a single '$'.  This could be
70# prevented by VARE_EVAL_KEEP_DOLLAR, but that flag is usually removed
71# before expanding subexpressions.  See ApplyModifier_Loop and
72# ParseModifierPart for examples.
73#
74.MAKEFLAGS: -dcp
75USE_8_DOLLARS=	${:U1:@var@${8_DOLLARS}@} ${8_DOLLARS} $$$$$$$$
76.if ${USE_8_DOLLARS} != "\$\$\$\$ \$\$\$\$ \$\$\$\$"
77.  error
78.endif
79#
80SUBST_CONTAINING_LOOP:= ${USE_8_DOLLARS}
81# The ':=' assignment operator evaluates the variable value using the mode
82# VARE_KEEP_DOLLAR_UNDEF, which means that some dollar signs are preserved,
83# but not all.  The dollar signs in the top-level expression and in the
84# indirect ${8_DOLLARS} are preserved.
85#
86# The variable modifier :@var@ does not preserve the dollar signs though, no
87# matter in which context it is evaluated.  What happens in detail is:
88# First, the modifier part "${8_DOLLARS}" is parsed without expanding it.
89# Next, each word of the value is expanded on its own, and at this moment
90# in ApplyModifier_Loop, the flag keepDollar is not passed down to
91# ModifyWords, resulting in "$$$$" for the first word of USE_8_DOLLARS.
92#
93# The remaining words of USE_8_DOLLARS are not affected by any variable
94# modifier and are thus expanded with the flag keepDollar in action.
95# The variable SUBST_CONTAINING_LOOP therefore gets assigned the raw value
96# "$$$$ $$$$$$$$ $$$$$$$$".
97#
98# The variable expression in the condition then expands this raw stored value
99# once, resulting in "$$ $$$$ $$$$".  The effects from VARE_KEEP_DOLLAR no
100# longer take place since they had only been active during the evaluation of
101# the variable assignment.
102.if ${SUBST_CONTAINING_LOOP} != "\$\$ \$\$\$\$ \$\$\$\$"
103.  error
104.endif
105.MAKEFLAGS: -d0
106
107# After looping over the words of the expression, the loop variable gets
108# undefined.  The modifier ':@' uses an ordinary global variable for this,
109# which is different from the '.for' loop, which replaces ${var} with
110# ${:Uvalue} in the body of the loop.  This choice of implementation detail
111# can be used for a nasty side effect.  The expression ${:U:@VAR@@} evaluates
112# to an empty string, plus it undefines the variable 'VAR'.  This is the only
113# possibility to undefine a global variable during evaluation.
114GLOBAL=		before-global
115RESULT:=	${:U${GLOBAL} ${:U:@GLOBAL@@} ${GLOBAL:Uundefined}}
116.if ${RESULT} != "before-global  undefined"
117.  error
118.endif
119
120# The above side effect of undefining a variable from a certain scope can be
121# further combined with the otherwise undocumented implementation detail that
122# the argument of an '.if' directive is evaluated in cmdline scope.  Putting
123# these together makes it possible to undefine variables from the cmdline
124# scope, something that is not possible in a straight-forward way.
125.MAKEFLAGS: CMDLINE=cmdline
126.if ${:U${CMDLINE}${:U:@CMDLINE@@}} != "cmdline"
127.  error
128.endif
129# Now the cmdline variable got undefined.
130.if ${CMDLINE} != "cmdline"
131.  error
132.endif
133# At this point, it still looks as if the cmdline variable were defined,
134# since the value of CMDLINE is still "cmdline".  That impression is only
135# superficial though, the cmdline variable is actually deleted.  To
136# demonstrate this, it is now possible to override its value using a global
137# variable, something that was not possible before:
138CMDLINE=	global
139.if ${CMDLINE} != "global"
140.  error
141.endif
142# Now undefine that global variable again, to get back to the original value.
143.undef CMDLINE
144.if ${CMDLINE} != "cmdline"
145.  error
146.endif
147# What actually happened is that when CMDLINE was set by the '.MAKEFLAGS'
148# target in the cmdline scope, that same variable was exported to the
149# environment, see Var_SetWithFlags.
150.unexport CMDLINE
151.if ${CMDLINE} != "cmdline"
152.  error
153.endif
154# The above '.unexport' has no effect since UnexportVar requires a global
155# variable of the same name to be defined, otherwise nothing is unexported.
156CMDLINE=	global
157.unexport CMDLINE
158.undef CMDLINE
159.if ${CMDLINE} != "cmdline"
160.  error
161.endif
162# This still didn't work since there must not only be a global variable, the
163# variable must be marked as exported as well, which it wasn't before.
164CMDLINE=	global
165.export CMDLINE
166.unexport CMDLINE
167.undef CMDLINE
168.if ${CMDLINE:Uundefined} != "undefined"
169.  error
170.endif
171# Finally the variable 'CMDLINE' from the cmdline scope is gone, and all its
172# traces from the environment are gone as well.  To do that, a global variable
173# had to be defined and exported, something that is far from obvious.  To
174# recap, here is the essence of the above story:
175.MAKEFLAGS: CMDLINE=cmdline	# have a cmdline + environment variable
176.if ${:U:@CMDLINE@@}}		# undefine cmdline, keep environment
177.endif
178CMDLINE=	global		# needed for deleting the environment
179.export CMDLINE			# needed for deleting the environment
180.unexport CMDLINE		# delete the environment
181.undef CMDLINE			# delete the global helper variable
182.if ${CMDLINE:Uundefined} != "undefined"
183.  error			# 'CMDLINE' is gone now from all scopes
184.endif
185
186
187# TODO: Actually trigger the undefined behavior (use after free) that was
188#  already suspected in Var_Parse, in the comment 'the value of the variable
189#  must not change'.
190