2 # Try to remove '/' recursively.
4 # Copyright (C) 2013-2024 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 <https://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 # Fainthearted: 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 # Set this to 0 if you don't have a working gdb but would
40 # still like to run the test
43 if test $USE_GDB = 1; then
45 *darwin
*) skip_
'avoiding due to potentially non functioning gdb' ;;
49 # Use gdb to provide further protection by limiting calls to unlinkat().
50 ( timeout
10s gdb
--version ) > gdb.out
2>&1
51 case $
(cat gdb.out
) in
53 *) skip_
"can't run gdb";;
57 # Break on a line rather than a symbol, to cater for inline functions
58 break_src
="$abs_top_srcdir/src/remove.c"
59 break_line
=$
(grep -n ^excise
"$break_src") || framework_failure_
60 break_line
=$
(echo "$break_line" | cut
-d: -f1) || framework_failure_
61 break_line
="$break_src:$break_line"
64 cat > k.c
<<'EOF' || framework_failure_
69 int unlinkat (int dirfd, const char *pathname, int flags)
71 /* Prove that LD_PRELOAD works: create the evidence file "x". */
72 fclose (fopen ("x", "w"));
74 /* Immediately terminate, unless indicated otherwise. */
75 if (! getenv("CU_TEST_SKIP_EXIT"))
78 /* Pretend success. */
83 # Then compile/link it:
84 gcc_shared_ k.c k.so \
85 || framework_failure_
'failed to build shared library'
87 # Note breakpoint commands don't work in batch mode
88 # https://sourceware.org/bugzilla/show_bug.cgi?id=10079
89 # So we use python to script behavior upon hitting the breakpoint
90 cat > bp.py
<<'EOF.py' || framework_failure_
91 def breakpoint_handler (event):
92 if not isinstance(event, gdb.BreakpointEvent):
94 hit_count = event.breakpoints[0].hit_count
96 gdb.execute('shell touch excise.break')
97 gdb.execute('continue')
99 gdb.write('breakpoint hit twice already')
100 gdb.execute('quit 1')
102 gdb.execute('continue')
104 gdb.events.stop.connect(breakpoint_handler)
107 # In order of the sed expressions below, this cleans:
109 # 1. gdb uses the full path when running rm, so remove the leading dirs.
110 # 2. For some of the "/" synonyms, the error diagnostic slightly differs from
111 # that of the basic "/" case (see gnulib's fts_open' and ROOT_DEV_INO_WARN):
112 # rm: it is dangerous to operate recursively on 'FILE' (same as '/')
113 # Strip that part off for the following comparison.
116 sed "s/.*rm: /rm: /; \
117 s/\(rm: it is dangerous to operate recursively on\).*$/\1 '\/'/"
120 #-------------------------------------------------------------------------------
121 # exercise_rm_r_root: shell function to test "rm -r '/'"
122 # The caller must provide the FILE to remove as well as any options
123 # which should be passed to 'rm'.
125 # For the worst case where both rm(1) would fail to refuse to process the "/"
126 # argument (in the cases without the --no-preserve-root option), and
127 # intercepting the unlinkat(1) system call would fail (which actually already
128 # has been proven to work above), and the current non root user has
129 # write access to "/", limit the damage to the current file system via
130 # the --one-file-system option.
131 # Furthermore, run rm(1) via gdb that limits the number of unlinkat() calls.
132 exercise_rm_r_root
()
134 # Remove the evidence files; verify that.
135 rm -f x excise.
break || framework_failure_
136 test -f x
&& framework_failure_
137 test -f excise.
break && framework_failure_
140 if [ "$CU_TEST_SKIP_EXIT" = 1 ]; then
141 # Pass on this variable into 'rm's environment.
142 skip_exit
='CU_TEST_SKIP_EXIT=1'
145 if test $USE_GDB = 1; then
146 gdb
-nx --batch-silent -return-child-result \
147 --eval-command="set exec-wrapper \
148 env 'LD_PRELOAD=$LD_PRELOAD:./k.so' $skip_exit" \
149 --eval-command="break '$break_line'" \
150 --eval-command='source bp.py' \
151 --eval-command="run -rv --one-file-system $*" \
152 --eval-command='quit' \
153 rm < /dev
/null
> out
2> err.t
156 env LD_PRELOAD
=$LD_PRELOAD:.
/k.so
$skip_exit \
157 rm -rv --one-file-system $
* < /dev
/null
> out
2> err.t
162 clean_rm_err_
< err.t
> err || ret
=$?
167 # Verify that "rm -r dir" basically works.
168 mkdir dir || framework_failure_
169 rm -r dir || framework_failure_
170 test -d dir
&& framework_failure_
172 # Now verify that intercepting unlinkat() works:
173 # rm(1) must succeed as before, but this time both the evidence file "x"
174 # and the test file / directory must still exist afterward.
175 mkdir dir || framework_failure_
176 > file || framework_failure_
179 for file in dir
file ; do
180 exercise_rm_r_root
"$file" || skip
=1
181 test -e "$file" || skip
=1
183 test -f excise.
break || skip
=1 # gdb works and breakpoint hit
184 compare
/dev
/null err || skip
=1
187 && { cat out
; cat err
; \
188 skip_
"internal test failure: maybe LD_PRELOAD or gdb doesn't work?"; }
191 # "rm -r /" without --no-preserve-root should output the following
192 # diagnostic error message.
193 cat <<EOD > exp || framework_failure_
194 rm: it is dangerous to operate recursively on '/'
195 rm: use --no-preserve-root to override this failsafe
198 #-------------------------------------------------------------------------------
199 # Exercise "rm -r /" without and with the --preserve-root option.
200 # Exercise various synonyms of "/" including symlinks to it.
201 # Expect a non-Zero exit status.
202 # Prepare a few symlinks to "/".
203 ln -s / rootlink || framework_failure_
204 ln -s rootlink rootlink2 || framework_failure_
205 ln -sr / rootlink3 || framework_failure_
209 '--preserve-root /' \
217 returns_
1 exercise_rm_r_root
$opts || fail
=1
219 # Expect nothing in 'out' and the above error diagnostic in 'err'.
220 # As rm(1) should have skipped the "/" argument, it does not call unlinkat().
221 # Therefore, the evidence file "x" should not exist.
222 compare
/dev
/null out || fail
=1
223 compare exp err || fail
=1
226 # Do nothing more if this test failed.
227 test $fail = 1 && { cat out
; cat err
; Exit
$fail; }
230 #-------------------------------------------------------------------------------
231 # Exercise with --no-preserve to ensure shortened equivalent is not allowed.
232 cat <<EOD > exp_opt || framework_failure_
233 rm: you may not abbreviate the --no-preserve-root option
235 returns_
1 exercise_rm_r_root
--no-preserve / || fail
=1
236 compare exp_opt err || fail
=1
239 #-------------------------------------------------------------------------------
240 # Exercise "rm -r file1 / file2".
241 # Expect a non-Zero exit status representing failure to remove "/",
242 # yet 'file1' and 'file2' should be removed.
243 > file1 || framework_failure_
244 > file2 || framework_failure_
246 # Now that we know that 'rm' won't call the unlinkat() system function for "/",
247 # we could probably execute it without the LD_PRELOAD'ed safety net.
248 # Nevertheless, it's still better to use it for this test.
249 # Tell the unlinkat() replacement function to not _exit(0) immediately
250 # by setting the following variable.
253 returns_
1 exercise_rm_r_root
--preserve-root file1
'/' file2 || fail
=1
255 unset CU_TEST_SKIP_EXIT
257 cat <<EOD > out_removed
262 # The above error diagnostic should appear in 'err'.
263 # Both 'file1' and 'file2' should be removed. Simply verify that in the
264 # "out" file, as the replacement unlinkat() dummy did not remove them.
265 # Expect the evidence file "x" to exist.
266 compare out_removed out || fail
=1
267 compare exp err || fail
=1
270 # Do nothing more if this test failed.
271 test $fail = 1 && { cat out
; cat err
; Exit
$fail; }
273 #-------------------------------------------------------------------------------
274 # Exercise various synonyms of "/" having a trailing "." or ".." in the name.
275 # This triggers another check in the code first and therefore leads to a
276 # different diagnostic. However, we want to test anyway to protect against
277 # future reordering of the checks in the code.
278 # Expect that other error diagnostic in 'err' and nothing in 'out'.
279 # Expect a non-Zero exit status. The evidence file "x" should not exist.
291 test -d "$file" ||
continue # if e.g. /etc does not exist.
293 returns_
1 exercise_rm_r_root
--preserve-root "$file" || fail
=1
295 grep "rm: refusing to remove '\.' or '\.\.' directory: skipping" err \
298 compare
/dev
/null out || fail
=1
301 # Do nothing more if this test failed.
302 test $fail = 1 && { cat out
; cat err
; Exit
$fail; }
305 #-------------------------------------------------------------------------------
306 # Until now, it was all just fun.
307 # Now exercise the --no-preserve-root option with which rm(1) should enter
308 # the intercepted unlinkat() system call.
309 # As the interception code terminates the process immediately via _exit(0),
310 # the exit status should be 0.
311 # Use the option --interactive=never to bypass the following prompt:
312 # "rm: descend into write-protected directory '/'?"
313 exercise_rm_r_root
--interactive=never
--no-preserve-root '/' \
316 # The 'err' file should not contain the above error diagnostic.
317 grep "rm: it is dangerous to operate recursively on '/'" err
&& fail
=1
319 # Instead, rm(1) should have called the intercepted unlinkat() function,
320 # i.e., the evidence file "x" should exist.
323 test $fail = 1 && { cat out
; cat err
; }