Skip to content

Commit 2ce39de

Browse files
committed
add a github check for programs not using traversal
1 parent d971b4f commit 2ce39de

File tree

3 files changed

+251
-0
lines changed

3 files changed

+251
-0
lines changed

.github/workflows/CICD.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,23 @@ jobs:
12741274
- name: Lint with SELinux
12751275
run: lima bash -c "cd work && cargo clippy --all-targets --features 'feat_selinux' -- -D warnings"
12761276

1277+
test_safe_traversal:
1278+
name: Safe Traversal Security Check
1279+
runs-on: ubuntu-latest
1280+
needs: [ min_version, deps ]
1281+
steps:
1282+
- uses: actions/checkout@v5
1283+
with:
1284+
persist-credentials: false
1285+
- uses: dtolnay/rust-toolchain@stable
1286+
- uses: Swatinem/rust-cache@v2
1287+
- name: Install strace
1288+
run: sudo apt-get update && sudo apt-get install -y strace
1289+
- name: Build utilities with safe traversal
1290+
run: cargo build --release -p uu_rm -p uu_chmod -p uu_chown -p uu_chgrp -p uu_mv -p uu_du
1291+
- name: Run safe traversal verification
1292+
run: ./util/check-safe-traversal.sh
1293+
12771294
benchmarks:
12781295
name: Run benchmarks (CodSpeed)
12791296
runs-on: ubuntu-latest

.vscode/cSpell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"docs/src/release-notes/**",
3535
"src/uu/*/benches/*.rs",
3636
"src/uucore/src/lib/features/benchmark.rs",
37+
"util/check-safe-traversal.sh",
3738
],
3839

3940
"enableGlobDot": true,

util/check-safe-traversal.sh

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
#!/bin/bash
2+
#
3+
# Check that utilities are using safe traversal (openat family syscalls)
4+
# to prevent TOCTOU race conditions
5+
#
6+
7+
set -e
8+
9+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
11+
TEMP_DIR=$(mktemp -d)
12+
13+
# Function to exit immediately on error
14+
fail_immediately() {
15+
echo "❌ FAILED: $1"
16+
echo ""
17+
echo "Debug information available in: $TEMP_DIR/strace_*.log"
18+
exit 1
19+
}
20+
21+
cleanup() {
22+
rm -rf "$TEMP_DIR"
23+
}
24+
trap cleanup EXIT
25+
26+
echo "=== Safe Traversal Verification ==="
27+
28+
# Build utilities if not already built
29+
if [ ! -f "$PROJECT_ROOT/target/release/rm" ]; then
30+
echo "Building utilities..."
31+
cd "$PROJECT_ROOT"
32+
cargo build --release --quiet
33+
fi
34+
35+
# Check if we should use individual binaries or multicall binary
36+
# Prefer individual binaries for more accurate testing
37+
if [ -f "$PROJECT_ROOT/target/release/rm" ]; then
38+
echo "Using individual binaries (preferred for testing)"
39+
USE_MULTICALL=0
40+
elif [ -f "$PROJECT_ROOT/target/release/coreutils" ]; then
41+
echo "Using multicall binary: $PROJECT_ROOT/target/release/coreutils"
42+
USE_MULTICALL=1
43+
COREUTILS_BIN="$PROJECT_ROOT/target/release/coreutils"
44+
else
45+
echo "No binaries found - please build first"
46+
exit 1
47+
fi
48+
49+
cd "$TEMP_DIR"
50+
51+
# Create test directory structure
52+
mkdir -p test_dir/sub1/sub2/sub3
53+
echo "test1" > test_dir/file1.txt
54+
echo "test2" > test_dir/sub1/file2.txt
55+
echo "test3" > test_dir/sub1/sub2/file3.txt
56+
echo "test4" > test_dir/sub1/sub2/sub3/file4.txt
57+
58+
check_utility() {
59+
local util="$1"
60+
local trace_syscalls="$2"
61+
local expected_syscalls="$3"
62+
local test_args="$4"
63+
local test_name="$5"
64+
65+
echo ""
66+
echo "Testing $util ($test_name)..."
67+
68+
local strace_log="strace_${util}_${test_name}.log"
69+
70+
# Choose binary to use
71+
if [ "$USE_MULTICALL" -eq 1 ]; then
72+
local util_cmd="$COREUTILS_BIN $util"
73+
else
74+
local util_path="$PROJECT_ROOT/target/release/$util"
75+
if [ ! -f "$util_path" ]; then
76+
fail_immediately "$util binary not found at $util_path"
77+
fi
78+
local util_cmd="$util_path"
79+
fi
80+
81+
# Run utility under strace
82+
strace -f -e trace="$trace_syscalls" -o "$strace_log" \
83+
$util_cmd $test_args 2>/dev/null || true
84+
cat $strace_log
85+
# Check for expected safe syscalls
86+
local found_safe=0
87+
for syscall in $expected_syscalls; do
88+
if grep -q "$syscall" "$strace_log"; then
89+
echo "✓ Found $syscall() (safe traversal)"
90+
found_safe=$((found_safe + 1))
91+
else
92+
fail_immediately "Missing $syscall() (safe traversal not active for $util)"
93+
fi
94+
done
95+
96+
# Count detailed syscall statistics
97+
local openat_count unlinkat_count fchmodat_count fchownat_count newfstatat_count renameat_count
98+
local unlink_count rmdir_count chmod_count chown_count safe_ops unsafe_ops
99+
100+
openat_count=$(grep -c "openat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
101+
unlinkat_count=$(grep -c "unlinkat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
102+
fchmodat_count=$(grep -c "fchmodat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
103+
fchownat_count=$(grep -c "fchownat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
104+
newfstatat_count=$(grep -c "newfstatat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
105+
renameat_count=$(grep -c "renameat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
106+
107+
# Count old unsafe syscalls (exclude the trace line prefix)
108+
unlink_count=$(grep -cE "\bunlink\(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
109+
rmdir_count=$(grep -cE "\brmdir\(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
110+
chmod_count=$(grep -cE "\bchmod\(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
111+
chown_count=$(grep -cE "\b(chown|lchown)\(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
112+
113+
# Ensure all variables are integers
114+
[ -z "$openat_count" ] && openat_count=0
115+
[ -z "$unlinkat_count" ] && unlinkat_count=0
116+
[ -z "$fchmodat_count" ] && fchmodat_count=0
117+
[ -z "$fchownat_count" ] && fchownat_count=0
118+
[ -z "$newfstatat_count" ] && newfstatat_count=0
119+
[ -z "$renameat_count" ] && renameat_count=0
120+
[ -z "$unlink_count" ] && unlink_count=0
121+
[ -z "$rmdir_count" ] && rmdir_count=0
122+
[ -z "$chmod_count" ] && chmod_count=0
123+
[ -z "$chown_count" ] && chown_count=0
124+
125+
# Calculate totals
126+
safe_ops=$((openat_count + unlinkat_count + fchmodat_count + fchownat_count + newfstatat_count + renameat_count))
127+
unsafe_ops=$((unlink_count + rmdir_count + chmod_count + chown_count))
128+
129+
echo " Strace statistics:"
130+
echo " Safe syscalls: openat=$openat_count unlinkat=$unlinkat_count fchmodat=$fchmodat_count fchownat=$fchownat_count newfstatat=$newfstatat_count renameat=$renameat_count"
131+
echo " Unsafe syscalls: unlink=$unlink_count rmdir=$rmdir_count chmod=$chmod_count chown/lchown=$chown_count"
132+
echo " Total: safe=$safe_ops unsafe=$unsafe_ops"
133+
134+
# For rm specifically, we expect unlinkat instead of unlink/rmdir for file operations
135+
# Note: A single rmdir() for the root directory is acceptable because:
136+
# 1. The root directory path is provided by the user (not discovered during traversal)
137+
# 2. There's no TOCTOU race - we're not resolving paths during recursive operations
138+
# 3. After safe traversal removes all contents via unlinkat(), rmdir() is safe for the empty root
139+
if [ "$util" = "rm" ]; then
140+
if [ "$unlinkat_count" -gt 0 ] && [ "$unlink_count" -eq 0 ] && [ "$rmdir_count" -le 1 ]; then
141+
echo "✓ Using safe syscalls (unlinkat for traversal)"
142+
if [ "$rmdir_count" -eq 1 ]; then
143+
echo " Note: Single rmdir() for root directory is acceptable"
144+
fi
145+
elif [ "$unlink_count" -gt 0 ] || [ "$rmdir_count" -gt 1 ]; then
146+
fail_immediately "$util is UNSAFE: Using unlink/rmdir for file operations (unlink=$unlink_count rmdir=$rmdir_count unlinkat=$unlinkat_count) - vulnerable to TOCTOU attacks"
147+
else
148+
echo "⚠ No file removal operations detected"
149+
fi
150+
elif [ "$safe_ops" -gt 0 ] && [ "$unsafe_ops" -eq 0 ]; then
151+
echo "✓ Using only safe syscalls"
152+
elif [ "$safe_ops" -gt 0 ] && [ "$safe_ops" -ge "$unsafe_ops" ]; then
153+
echo "✓ Using primarily safe syscalls"
154+
elif [ "$found_safe" -gt 0 ]; then
155+
echo "⚠ Some safe syscalls found but mixed with unsafe ops"
156+
else
157+
fail_immediately "$util is not using safe traversal"
158+
fi
159+
}
160+
161+
# Get list of available utilities
162+
if [ "$USE_MULTICALL" -eq 1 ]; then
163+
AVAILABLE_UTILS=$($COREUTILS_BIN --list)
164+
else
165+
AVAILABLE_UTILS=""
166+
for util in rm chmod chown chgrp du mv; do
167+
if [ -f "$PROJECT_ROOT/target/release/$util" ]; then
168+
AVAILABLE_UTILS="$AVAILABLE_UTILS $util"
169+
fi
170+
done
171+
fi
172+
173+
# Test rm - should use openat, unlinkat, newfstatat
174+
if echo "$AVAILABLE_UTILS" | grep -q "rm"; then
175+
cp -r test_dir test_rm
176+
check_utility "rm" "openat,unlinkat,newfstatat,unlink,rmdir" "openat" "-rf test_rm" "recursive_remove"
177+
fi
178+
179+
# Test chmod - should use openat, fchmodat, newfstatat
180+
if echo "$AVAILABLE_UTILS" | grep -q "chmod"; then
181+
cp -r test_dir test_chmod
182+
check_utility "chmod" "openat,fchmodat,newfstatat,chmod" "openat fchmodat" "-R 755 test_chmod" "recursive_chmod"
183+
fi
184+
185+
# Test chown - should use openat, fchownat, newfstatat
186+
if echo "$AVAILABLE_UTILS" | grep -q "chown"; then
187+
cp -r test_dir test_chown
188+
USER_ID=$(id -u)
189+
GROUP_ID=$(id -g)
190+
check_utility "chown" "openat,fchownat,newfstatat,chown,lchown" "openat fchownat" "-R $USER_ID:$GROUP_ID test_chown" "recursive_chown"
191+
fi
192+
193+
# Test chgrp - should use openat, fchownat, newfstatat
194+
if echo "$AVAILABLE_UTILS" | grep -q "chgrp"; then
195+
cp -r test_dir test_chgrp
196+
check_utility "chgrp" "openat,fchownat,newfstatat,chown,lchown" "openat fchownat" "-R $GROUP_ID test_chgrp" "recursive_chgrp"
197+
fi
198+
199+
# Test du - should use openat, newfstatat
200+
if echo "$AVAILABLE_UTILS" | grep -q "du"; then
201+
cp -r test_dir test_du
202+
check_utility "du" "openat,newfstatat,stat,lstat" "openat" "-a test_du" "directory_usage"
203+
fi
204+
205+
# Test mv - should use openat, renameat for directory moves
206+
if echo "$AVAILABLE_UTILS" | grep -q "mv"; then
207+
mkdir -p test_mv_src/sub
208+
echo "test" > test_mv_src/file.txt
209+
echo "test" > test_mv_src/sub/file2.txt
210+
check_utility "mv" "openat,renameat,newfstatat,rename" "openat" "test_mv_src test_mv_dst" "move_directory"
211+
fi
212+
213+
echo ""
214+
echo "=== Additional Safety Checks ==="
215+
216+
# Check for dangerous patterns across all logs
217+
echo "Checking for dangerous path resolution patterns..."
218+
echo "✓ Basic safe traversal verification completed"
219+
220+
# Check that we're not doing excessive path resolutions (sign of TOCTOU vulnerability)
221+
echo "Checking path resolution frequency..."
222+
for log in strace_*.log; do
223+
if [ -f "$log" ]; then
224+
path_resolutions=$(grep -c "test_" "$log" 2>/dev/null || echo "0")
225+
if [ "$path_resolutions" -gt 20 ]; then
226+
echo "$log: High path resolution count ($path_resolutions) - potential TOCTOU risk"
227+
fi
228+
fi
229+
done
230+
231+
echo ""
232+
echo "=== Summary ==="
233+
echo "All utilities are using safe traversal correctly!"

0 commit comments

Comments
 (0)