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
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.
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.
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
43 *) skip_
"can't run gdb";;
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_
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"))
67 /* Pretend success. */
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):
83 hit_count = event.breakpoints[0].hit_count
85 gdb.execute('shell touch excise.break')
86 gdb.execute('continue')
88 gdb.write('breakpoint hit twice already')
91 gdb.execute('continue')
93 gdb.events.stop.connect(breakpoint_handler)
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.
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'.
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_
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
145 clean_rm_err_
< err.t
> err || 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_
162 for file in dir
file ; do
163 exercise_rm_r_root
"$file" || skip
=1
164 test -e "$file" || skip
=1
166 test -f excise.
break || skip
=1 # gdb works and breakpoint hit
167 compare
/dev
/null err || skip
=1
170 && { cat out
; cat err
; \
171 skip_
"internal test failure: maybe LD_PRELOAD or gdb doesn't work?"; }
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_
192 '--preserve-root /' \
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
209 # Do nothing more if this test failed.
210 test $fail = 1 && { cat out
; cat err
; Exit
$fail; }
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.
227 returns_
1 exercise_rm_r_root
--preserve-root file1
'/' file2 || fail
=1
229 unset CU_TEST_SKIP_EXIT
231 cat <<EOD > out_removed
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
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.
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 \
272 compare
/dev
/null out || fail
=1
275 # Do nothing more if this test failed.
276 test $fail = 1 && { cat out
; cat err
; Exit
$fail; }
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 '/' \
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.
297 test $fail = 1 && { cat out
; cat err
; }