Skip to content

Commit 221e3b9

Browse files
committed
add a github check for programs not using traversal
1 parent 7bca02c commit 221e3b9

File tree

2 files changed

+237
-0
lines changed

2 files changed

+237
-0
lines changed

.github/workflows/CICD.yml

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

1225+
test_safe_traversal:
1226+
name: Safe Traversal Security Check
1227+
runs-on: ubuntu-latest
1228+
needs: [ min_version, deps ]
1229+
steps:
1230+
- uses: actions/checkout@v5
1231+
with:
1232+
persist-credentials: false
1233+
- uses: dtolnay/rust-toolchain@stable
1234+
- uses: Swatinem/rust-cache@v2
1235+
- name: Install strace
1236+
run: sudo apt-get update && sudo apt-get install -y strace
1237+
- name: Build utilities with safe traversal
1238+
run: cargo build --release --bin rm --bin chmod --bin chown --bin chgrp --bin mv --bin du
1239+
- name: Run safe traversal verification
1240+
run: ./util/check-safe-traversal.sh
1241+
12251242
benchmarks:
12261243
name: Run benchmarks (CodSpeed)
12271244
runs-on: ubuntu-latest

util/check-safe-traversal.sh

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

0 commit comments

Comments
 (0)