|
| 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