test: skip test rather than fail with Solaris 10 sed
[coreutils.git] / tests / rm / r-root.sh
blobb98db141be98b222d4ab2277d1b166c9e959d62d
1 #!/bin/sh
2 # Try to remove '/' recursively.
4 # Copyright (C) 2013-2016 Free Software Foundation, Inc.
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 . "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
20 print_ver_ rm
22 # POSIX mandates rm(1) to skip '/' arguments. This test verifies this mandated
23 # behavior as well as the --preserve-root and --no-preserve-root options.
24 # Especially the latter case is a live fire exercise as rm(1) is supposed to
25 # enter the unlinkat() system call. Therefore, limit the risk as much
26 # as possible -- if there's a bug this test would wipe the system out!
28 # Faint-hearted: skip this test for the 'root' user.
29 skip_if_root_
31 # Pull the teeth from rm(1) by intercepting the unlinkat() system call via the
32 # LD_PRELOAD environment variable. This requires shared libraries to work.
33 require_gcc_shared_
35 # Ensure this variable is unset as it's
36 # used later in the unlinkat() wrapper.
37 unset CU_TEST_SKIP_EXIT
39 # Use gdb to provide further protection by limiting calls to unlinkat().
40 ( timeout 10s gdb --version ) > gdb.out 2>&1
41 case $(cat gdb.out) in
42 *'GNU gdb'*) ;;
43 *) skip_ "can't run gdb";;
44 esac
46 # Break on a line rather than a symbol, to cater for inline functions
47 break_src="$abs_top_srcdir/src/remove.c"
48 break_line=$(grep -n ^excise "$break_src") || framework_failure_
49 break_line=$(echo "$break_line" | cut -d: -f1) || framework_failure_
50 break_line="$break_src:$break_line"
53 cat > k.c <<'EOF' || framework_failure_
54 #include <stdio.h>
55 #include <stdlib.h>
56 #include <unistd.h>
58 int unlinkat (int dirfd, const char *pathname, int flags)
60 /* Prove that LD_PRELOAD works: create the evidence file "x". */
61 fclose (fopen ("x", "w"));
63 /* Immediately terminate, unless indicated otherwise. */
64 if (! getenv("CU_TEST_SKIP_EXIT"))
65 _exit (0);
67 /* Pretend success. */
68 return 0;
70 EOF
72 # Then compile/link it:
73 gcc_shared_ k.c k.so \
74 || framework_failure_ 'failed to build shared library'
76 # Note breakpoint commands don't work in batch mode
77 # https://sourceware.org/bugzilla/show_bug.cgi?id=10079
78 # So we use python to script behavior upon hitting the breakpoint
79 cat > bp.py <<'EOF.py' || framework_failure_
80 def breakpoint_handler (event):
81 if not isinstance(event, gdb.BreakpointEvent):
82 return
83 hit_count = event.breakpoints[0].hit_count
84 if hit_count == 1:
85 gdb.execute('shell touch excise.break')
86 gdb.execute('continue')
87 elif hit_count > 2:
88 gdb.write('breakpoint hit twice already')
89 gdb.execute('quit 1')
90 else:
91 gdb.execute('continue')
93 gdb.events.stop.connect(breakpoint_handler)
94 EOF.py
96 # In order of the sed expressions below, this cleans:
98 # 1. gdb uses the full path when running rm, so remove the leading dirs.
99 # 2. For some of the "/" synonyms, the error diagnostic slightly differs from
100 # that of the basic "/" case (see gnulib's fts_open' and ROOT_DEV_INO_WARN):
101 # rm: it is dangerous to operate recursively on 'FILE' (same as '/')
102 # Strip that part off for the following comparison.
103 clean_rm_err_()
105 sed "s/.*rm: /rm: /; \
106 s/\(rm: it is dangerous to operate recursively on\).*$/\1 '\/'/"
109 #-------------------------------------------------------------------------------
110 # exercise_rm_r_root: shell function to test "rm -r '/'"
111 # The caller must provide the FILE to remove as well as any options
112 # which should be passed to 'rm'.
113 # Paranoia mode on:
114 # For the worst case where both rm(1) would fail to refuse to process the "/"
115 # argument (in the cases without the --no-preserve-root option), and
116 # intercepting the unlinkat(1) system call would fail (which actually already
117 # has been proven to work above), and the current non root user has
118 # write access to "/", limit the damage to the current file system via
119 # the --one-file-system option.
120 # Furthermore, run rm(1) via gdb that limits the number of unlinkat() calls.
121 exercise_rm_r_root ()
123 # Remove the evidence files; verify that.
124 rm -f x excise.break || framework_failure_
125 test -f x && framework_failure_
126 test -f excise.break && framework_failure_
128 local skip_exit=
129 if [ "$CU_TEST_SKIP_EXIT" = 1 ]; then
130 # Pass on this variable into 'rm's environment.
131 skip_exit='CU_TEST_SKIP_EXIT=1'
134 gdb -nx --batch-silent -return-child-result \
135 --eval-command="set exec-wrapper \
136 env 'LD_PRELOAD=$LD_PRELOAD:./k.so' $skip_exit" \
137 --eval-command="break '$break_line'" \
138 --eval-command='source bp.py' \
139 --eval-command="run -rv --one-file-system $*" \
140 --eval-command='quit' \
141 rm < /dev/null > out 2> err.t
143 ret=$?
145 clean_rm_err_ < err.t > err || ret=$?
147 return $ret
150 # Verify that "rm -r dir" basically works.
151 mkdir dir || framework_failure_
152 rm -r dir || framework_failure_
153 test -d dir && framework_failure_
155 # Now verify that intercepting unlinkat() works:
156 # rm(1) must succeed as before, but this time both the evidence file "x"
157 # and the test file / directory must still exist afterward.
158 mkdir dir || framework_failure_
159 > file || framework_failure_
161 skip=
162 for file in dir file ; do
163 exercise_rm_r_root "$file" || skip=1
164 test -e "$file" || skip=1
165 test -f x || skip=1
166 test -f excise.break || skip=1 # gdb works and breakpoint hit
167 compare /dev/null err || skip=1
169 test "$skip" = 1 \
170 && { cat out; cat err; \
171 skip_ "internal test failure: maybe LD_PRELOAD or gdb doesn't work?"; }
172 done
174 # "rm -r /" without --no-preserve-root should output the following
175 # diagnostic error message.
176 cat <<EOD > exp || framework_failure_
177 rm: it is dangerous to operate recursively on '/'
178 rm: use --no-preserve-root to override this failsafe
181 #-------------------------------------------------------------------------------
182 # Exercise "rm -r /" without and with the --preserve-root option.
183 # Exercise various synonyms of "/" including symlinks to it.
184 # Expect a non-Zero exit status.
185 # Prepare a few symlinks to "/".
186 ln -s / rootlink || framework_failure_
187 ln -s rootlink rootlink2 || framework_failure_
188 ln -sr / rootlink3 || framework_failure_
190 for opts in \
191 '/' \
192 '--preserve-root /' \
193 '//' \
194 '///' \
195 '////' \
196 'rootlink/' \
197 'rootlink2/' \
198 'rootlink3/' ; do
200 returns_ 1 exercise_rm_r_root $opts || fail=1
202 # Expect nothing in 'out' and the above error diagnostic in 'err'.
203 # As rm(1) should have skipped the "/" argument, it does not call unlinkat().
204 # Therefore, the evidence file "x" should not exist.
205 compare /dev/null out || fail=1
206 compare exp err || fail=1
207 test -f x && fail=1
209 # Do nothing more if this test failed.
210 test $fail = 1 && { cat out; cat err; Exit $fail; }
211 done
213 #-------------------------------------------------------------------------------
214 # Exercise "rm -r file1 / file2".
215 # Expect a non-Zero exit status representing failure to remove "/",
216 # yet 'file1' and 'file2' should be removed.
217 > file1 || framework_failure_
218 > file2 || framework_failure_
220 # Now that we know that 'rm' won't call the unlinkat() system function for "/",
221 # we could probably execute it without the LD_PRELOAD'ed safety net.
222 # Nevertheless, it's still better to use it for this test.
223 # Tell the unlinkat() replacement function to not _exit(0) immediately
224 # by setting the following variable.
225 CU_TEST_SKIP_EXIT=1
227 returns_ 1 exercise_rm_r_root --preserve-root file1 '/' file2 || fail=1
229 unset CU_TEST_SKIP_EXIT
231 cat <<EOD > out_removed
232 removed 'file1'
233 removed 'file2'
236 # The above error diagnostic should appear in 'err'.
237 # Both 'file1' and 'file2' should be removed. Simply verify that in the
238 # "out" file, as the replacement unlinkat() dummy did not remove them.
239 # Expect the evidence file "x" to exist.
240 compare out_removed out || fail=1
241 compare exp err || fail=1
242 test -f x || fail=1
244 # Do nothing more if this test failed.
245 test $fail = 1 && { cat out; cat err; Exit $fail; }
247 #-------------------------------------------------------------------------------
248 # Exercise various synonyms of "/" having a trailing "." or ".." in the name.
249 # This triggers another check in the code first and therefore leads to a
250 # different diagnostic. However, we want to test anyway to protect against
251 # future reordering of the checks in the code.
252 # Expect that other error diagnostic in 'err' and nothing in 'out'.
253 # Expect a non-Zero exit status. The evidence file "x" should not exist.
254 for file in \
255 '//.' \
256 '/./' \
257 '/.//' \
258 '/../' \
259 '/.././' \
260 '/etc/..' \
261 'rootlink/..' \
262 'rootlink2/.' \
263 'rootlink3/./' ; do
265 test -d "$file" || continue # if e.g. /etc does not exist.
267 returns_ 1 exercise_rm_r_root --preserve-root "$file" || fail=1
269 grep "rm: refusing to remove '\.' or '\.\.' directory: skipping" err \
270 || fail=1
272 compare /dev/null out || fail=1
273 test -f x && fail=1
275 # Do nothing more if this test failed.
276 test $fail = 1 && { cat out; cat err; Exit $fail; }
277 done
279 #-------------------------------------------------------------------------------
280 # Until now, it was all just fun.
281 # Now exercise the --no-preserve-root option with which rm(1) should enter
282 # the intercepted unlinkat() system call.
283 # As the interception code terminates the process immediately via _exit(0),
284 # the exit status should be 0.
285 # Use the option --interactive=never to bypass the following prompt:
286 # "rm: descend into write-protected directory '/'?"
287 exercise_rm_r_root --interactive=never --no-preserve-root '/' \
288 || fail=1
290 # The 'err' file should not contain the above error diagnostic.
291 grep "rm: it is dangerous to operate recursively on '/'" err && fail=1
293 # Instead, rm(1) should have called the intercepted unlinkat() function,
294 # i.e., the evidence file "x" should exist.
295 test -f x || fail=1
297 test $fail = 1 && { cat out; cat err; }
299 Exit $fail