1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
"""Tests for the safe_rm_rf guard helper in the archsetup installer.
safe_rm_rf is a defensive wrapper around `rm -rf`: it refuses to delete a
path unless the path is absolute, free of '..', deeper than a bare
top-level dir, strictly inside a caller-supplied allowed prefix, and a real
directory (not a symlink). On the happy path it delegates to `rm -rf`.
These tests exercise the REAL function body, extracted from the `archsetup`
script at run time (not a copy), with a stub `error_warn` standing in for
the installer's logger. The delete runs against real temp dirs the test
creates and tears down, so the rm path is genuinely exercised.
Run from repo root:
python3 -m unittest tests.safe-rm-rf.test_safe_rm_rf
"""
import os
import shutil
import subprocess
import tempfile
import unittest
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
ARCHSETUP = os.path.join(REPO_ROOT, "archsetup")
class SafeRmRfHarness(unittest.TestCase):
"""Source safe_rm_rf out of the real archsetup script and invoke it."""
def setUp(self):
self.tmp = tempfile.mkdtemp(prefix="safe-rm-rf-test-")
# A bash wrapper that extracts just the safe_rm_rf function from the
# real installer and invokes it with the test's args. Sourcing the
# sed-extracted function means we test the production code path, not a
# reimplementation. The helper is self-contained (it prints its own
# refusal reasons), so no logger stub is needed.
self.wrapper = os.path.join(self.tmp, "run.sh")
with open(self.wrapper, "w") as f:
f.write(
"#!/bin/bash\n"
'ARCHSETUP="$1"; shift\n'
"source <(sed -n '/^safe_rm_rf() {/,/^}/p' \"$ARCHSETUP\")\n"
'safe_rm_rf "$@"\n'
)
os.chmod(self.wrapper, 0o755)
def tearDown(self):
shutil.rmtree(self.tmp, ignore_errors=True)
def run_guard(self, target, prefix):
return subprocess.run(
["bash", self.wrapper, ARCHSETUP, target, prefix],
capture_output=True, text=True, timeout=10,
)
def make_dir(self, *parts):
path = os.path.join(self.tmp, *parts)
os.makedirs(path, exist_ok=True)
return path
# -----------------------------------------------------------------------------
# Normal cases
# -----------------------------------------------------------------------------
class TestSafeRmRfNormal(SafeRmRfHarness):
def test_real_dir_under_prefix_is_deleted(self):
prefix = self.make_dir("src")
target = self.make_dir("src", "repo")
result = self.run_guard(target, prefix)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertFalse(os.path.exists(target), "target should be deleted")
self.assertTrue(os.path.isdir(prefix), "prefix must survive")
def test_nested_dir_with_contents_is_deleted(self):
prefix = self.make_dir("src")
target = self.make_dir("src", "repo", "sub")
with open(os.path.join(target, "file.txt"), "w") as f:
f.write("data")
result = self.run_guard(os.path.join(prefix, "repo"), prefix)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertFalse(os.path.exists(os.path.join(prefix, "repo")))
# -----------------------------------------------------------------------------
# Boundary cases
# -----------------------------------------------------------------------------
class TestSafeRmRfBoundary(SafeRmRfHarness):
def test_target_already_absent_succeeds_quietly(self):
prefix = self.make_dir("src")
target = os.path.join(prefix, "never-existed")
result = self.run_guard(target, prefix)
self.assertEqual(result.returncode, 0, msg=result.stderr)
def test_target_equal_to_prefix_is_refused(self):
prefix = self.make_dir("src")
result = self.run_guard(prefix, prefix)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(os.path.isdir(prefix), "prefix must not be deleted")
def test_symlink_to_dir_under_prefix_is_refused(self):
prefix = self.make_dir("src")
real = self.make_dir("src", "real")
link = os.path.join(prefix, "link")
os.symlink(real, link)
result = self.run_guard(link, prefix)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(os.path.isdir(real), "symlink target must survive")
self.assertTrue(os.path.islink(link), "symlink itself must survive")
def test_prefix_lookalike_is_refused(self):
# target shares a textual prefix but is not inside it:
# /.../srcX is NOT under /.../src
prefix = self.make_dir("src")
sibling = self.make_dir("srcX")
result = self.run_guard(sibling, prefix)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(os.path.isdir(sibling), "lookalike sibling must survive")
# -----------------------------------------------------------------------------
# Error cases
# -----------------------------------------------------------------------------
class TestSafeRmRfErrors(SafeRmRfHarness):
def test_empty_target_is_refused(self):
prefix = self.make_dir("src")
result = self.run_guard("", prefix)
self.assertNotEqual(result.returncode, 0)
def test_empty_prefix_is_refused(self):
target = self.make_dir("src", "repo")
result = self.run_guard(target, "")
self.assertNotEqual(result.returncode, 0)
self.assertTrue(os.path.isdir(target), "target must survive a bad call")
def test_relative_path_is_refused(self):
result = self.run_guard("foo/bar", "/var/lib/archsetup")
self.assertNotEqual(result.returncode, 0)
def test_root_is_refused(self):
result = self.run_guard("/", "/var/lib/archsetup")
self.assertNotEqual(result.returncode, 0)
def test_bare_top_level_dir_is_refused(self):
result = self.run_guard("/home", "/home")
self.assertNotEqual(result.returncode, 0)
self.assertTrue(os.path.isdir("/home"), "/home must never be touched")
def test_path_with_dotdot_is_refused(self):
prefix = self.make_dir("src")
# textually inside the prefix but contains a traversal segment
sneaky = os.path.join(prefix, "..", "src", "repo")
result = self.run_guard(sneaky, prefix)
self.assertNotEqual(result.returncode, 0)
def test_path_outside_prefix_is_refused(self):
prefix = self.make_dir("src")
outside = self.make_dir("other")
result = self.run_guard(outside, prefix)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(os.path.isdir(outside), "outside dir must survive")
if __name__ == "__main__":
unittest.main()
|