mirror of
https://github.com/uutils/coreutils.git
synced 2025-12-23 08:47:37 +00:00
add a github check for programs not using traversal
This commit is contained in:
parent
e4b86542d6
commit
d4e47861bb
3 changed files with 247 additions and 3 deletions
22
.github/workflows/CICD.yml
vendored
22
.github/workflows/CICD.yml
vendored
|
|
@ -1280,16 +1280,15 @@ jobs:
|
|||
job:
|
||||
- { os: macos-latest , features: feat_os_macos }
|
||||
- { os: windows-latest , features: feat_os_windows }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build SELinux utilities as stubs
|
||||
run: cargo build -p uu_chcon -p uu_runcon
|
||||
|
||||
- name: Verify stub binaries exist
|
||||
shell: bash
|
||||
run: |
|
||||
|
|
@ -1300,10 +1299,27 @@ jobs:
|
|||
test -f target/debug/chcon || exit 1
|
||||
test -f target/debug/runcon || exit 1
|
||||
fi
|
||||
|
||||
- name: Verify workspace builds with stubs
|
||||
run: cargo build --features ${{ matrix.job.features }}
|
||||
|
||||
test_safe_traversal:
|
||||
name: Safe Traversal Security Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ min_version, deps ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install strace
|
||||
run: sudo apt-get update && sudo apt-get install -y strace
|
||||
- name: Build utilities with safe traversal
|
||||
run: cargo build --release -p uu_rm -p uu_chmod -p uu_chown -p uu_chgrp -p uu_mv -p uu_du
|
||||
- name: Run safe traversal verification
|
||||
run: ./util/check-safe-traversal.sh
|
||||
|
||||
benchmarks:
|
||||
name: Run benchmarks (CodSpeed)
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
1
.vscode/cSpell.json
vendored
1
.vscode/cSpell.json
vendored
|
|
@ -34,6 +34,7 @@
|
|||
"docs/src/release-notes/**",
|
||||
"src/uu/*/benches/*.rs",
|
||||
"src/uucore/src/lib/features/benchmark.rs",
|
||||
"util/check-safe-traversal.sh",
|
||||
],
|
||||
|
||||
"enableGlobDot": true,
|
||||
|
|
|
|||
227
util/check-safe-traversal.sh
Executable file
227
util/check-safe-traversal.sh
Executable file
|
|
@ -0,0 +1,227 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Check that utilities are using safe traversal (openat family syscalls)
|
||||
# to prevent TOCTOU race conditions
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
|
||||
# Function to exit immediately on error
|
||||
fail_immediately() {
|
||||
echo "❌ FAILED: $1"
|
||||
echo ""
|
||||
echo "Debug information available in: $TEMP_DIR/strace_*.log"
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TEMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "=== Safe Traversal Verification ==="
|
||||
|
||||
# Assume binaries are already built (for CI usage)
|
||||
# Prefer individual binaries for more accurate testing
|
||||
if [ -f "$PROJECT_ROOT/target/release/rm" ]; then
|
||||
echo "Using individual binaries"
|
||||
USE_MULTICALL=0
|
||||
elif [ -f "$PROJECT_ROOT/target/release/coreutils" ]; then
|
||||
echo "Using multicall binary"
|
||||
USE_MULTICALL=1
|
||||
COREUTILS_BIN="$PROJECT_ROOT/target/release/coreutils"
|
||||
else
|
||||
echo "Error: No binaries found. Please build first with 'cargo build --release'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$TEMP_DIR"
|
||||
|
||||
# Create test directory structure
|
||||
mkdir -p test_dir/sub1/sub2/sub3
|
||||
echo "test1" > test_dir/file1.txt
|
||||
echo "test2" > test_dir/sub1/file2.txt
|
||||
echo "test3" > test_dir/sub1/sub2/file3.txt
|
||||
echo "test4" > test_dir/sub1/sub2/sub3/file4.txt
|
||||
|
||||
check_utility() {
|
||||
local util="$1"
|
||||
local trace_syscalls="$2"
|
||||
local expected_syscalls="$3"
|
||||
local test_args="$4"
|
||||
local test_name="$5"
|
||||
|
||||
echo ""
|
||||
echo "Testing $util ($test_name)..."
|
||||
|
||||
local strace_log="strace_${util}_${test_name}.log"
|
||||
|
||||
# Choose binary to use
|
||||
if [ "$USE_MULTICALL" -eq 1 ]; then
|
||||
local util_cmd="$COREUTILS_BIN $util"
|
||||
else
|
||||
local util_path="$PROJECT_ROOT/target/release/$util"
|
||||
if [ ! -f "$util_path" ]; then
|
||||
fail_immediately "$util binary not found at $util_path"
|
||||
fi
|
||||
local util_cmd="$util_path"
|
||||
fi
|
||||
|
||||
# Run utility under strace
|
||||
strace -f -e trace="$trace_syscalls" -o "$strace_log" \
|
||||
$util_cmd $test_args 2>/dev/null || true
|
||||
cat $strace_log
|
||||
# Check for expected safe syscalls
|
||||
local found_safe=0
|
||||
for syscall in $expected_syscalls; do
|
||||
if grep -q "$syscall" "$strace_log"; then
|
||||
echo "✓ Found $syscall() (safe traversal)"
|
||||
found_safe=$((found_safe + 1))
|
||||
else
|
||||
fail_immediately "Missing $syscall() (safe traversal not active for $util)"
|
||||
fi
|
||||
done
|
||||
|
||||
# Count detailed syscall statistics
|
||||
local openat_count unlinkat_count fchmodat_count fchownat_count newfstatat_count renameat_count
|
||||
local unlink_count rmdir_count chmod_count chown_count safe_ops unsafe_ops
|
||||
|
||||
openat_count=$(grep -c "openat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
|
||||
unlinkat_count=$(grep -c "unlinkat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
|
||||
fchmodat_count=$(grep -c "fchmodat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
|
||||
fchownat_count=$(grep -c "fchownat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
|
||||
newfstatat_count=$(grep -c "newfstatat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
|
||||
renameat_count=$(grep -c "renameat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
|
||||
|
||||
# Count old unsafe syscalls (exclude the trace line prefix)
|
||||
unlink_count=$(grep -cE "\bunlink\(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
|
||||
rmdir_count=$(grep -cE "\brmdir\(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
|
||||
chmod_count=$(grep -cE "\bchmod\(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
|
||||
chown_count=$(grep -cE "\b(chown|lchown)\(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0")
|
||||
|
||||
# Ensure all variables are integers
|
||||
[ -z "$openat_count" ] && openat_count=0
|
||||
[ -z "$unlinkat_count" ] && unlinkat_count=0
|
||||
[ -z "$fchmodat_count" ] && fchmodat_count=0
|
||||
[ -z "$fchownat_count" ] && fchownat_count=0
|
||||
[ -z "$newfstatat_count" ] && newfstatat_count=0
|
||||
[ -z "$renameat_count" ] && renameat_count=0
|
||||
[ -z "$unlink_count" ] && unlink_count=0
|
||||
[ -z "$rmdir_count" ] && rmdir_count=0
|
||||
[ -z "$chmod_count" ] && chmod_count=0
|
||||
[ -z "$chown_count" ] && chown_count=0
|
||||
|
||||
# Calculate totals
|
||||
safe_ops=$((openat_count + unlinkat_count + fchmodat_count + fchownat_count + newfstatat_count + renameat_count))
|
||||
unsafe_ops=$((unlink_count + rmdir_count + chmod_count + chown_count))
|
||||
|
||||
echo " Strace statistics:"
|
||||
echo " Safe syscalls: openat=$openat_count unlinkat=$unlinkat_count fchmodat=$fchmodat_count fchownat=$fchownat_count newfstatat=$newfstatat_count renameat=$renameat_count"
|
||||
echo " Unsafe syscalls: unlink=$unlink_count rmdir=$rmdir_count chmod=$chmod_count chown/lchown=$chown_count"
|
||||
echo " Total: safe=$safe_ops unsafe=$unsafe_ops"
|
||||
|
||||
# For rm specifically, we expect unlinkat instead of unlink/rmdir for file operations
|
||||
# Note: A single rmdir() for the root directory is acceptable because:
|
||||
# 1. The root directory path is provided by the user (not discovered during traversal)
|
||||
# 2. There's no TOCTOU race - we're not resolving paths during recursive operations
|
||||
# 3. After safe traversal removes all contents via unlinkat(), rmdir() is safe for the empty root
|
||||
if [ "$util" = "rm" ]; then
|
||||
if [ "$unlinkat_count" -gt 0 ] && [ "$unlink_count" -eq 0 ] && [ "$rmdir_count" -le 1 ]; then
|
||||
echo "✓ Using safe syscalls (unlinkat for traversal)"
|
||||
if [ "$rmdir_count" -eq 1 ]; then
|
||||
echo " Note: Single rmdir() for root directory is acceptable"
|
||||
fi
|
||||
elif [ "$unlink_count" -gt 0 ] || [ "$rmdir_count" -gt 1 ]; then
|
||||
fail_immediately "$util is UNSAFE: Using unlink/rmdir for file operations (unlink=$unlink_count rmdir=$rmdir_count unlinkat=$unlinkat_count) - vulnerable to TOCTOU attacks"
|
||||
else
|
||||
echo "⚠ No file removal operations detected"
|
||||
fi
|
||||
elif [ "$safe_ops" -gt 0 ] && [ "$unsafe_ops" -eq 0 ]; then
|
||||
echo "✓ Using only safe syscalls"
|
||||
elif [ "$safe_ops" -gt 0 ] && [ "$safe_ops" -ge "$unsafe_ops" ]; then
|
||||
echo "✓ Using primarily safe syscalls"
|
||||
elif [ "$found_safe" -gt 0 ]; then
|
||||
echo "⚠ Some safe syscalls found but mixed with unsafe ops"
|
||||
else
|
||||
fail_immediately "$util is not using safe traversal"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get list of available utilities
|
||||
if [ "$USE_MULTICALL" -eq 1 ]; then
|
||||
AVAILABLE_UTILS=$($COREUTILS_BIN --list)
|
||||
else
|
||||
AVAILABLE_UTILS=""
|
||||
for util in rm chmod chown chgrp du mv; do
|
||||
if [ -f "$PROJECT_ROOT/target/release/$util" ]; then
|
||||
AVAILABLE_UTILS="$AVAILABLE_UTILS $util"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Test rm - should use openat, unlinkat, newfstatat
|
||||
if echo "$AVAILABLE_UTILS" | grep -q "rm"; then
|
||||
cp -r test_dir test_rm
|
||||
check_utility "rm" "openat,unlinkat,newfstatat,unlink,rmdir" "openat" "-rf test_rm" "recursive_remove"
|
||||
fi
|
||||
|
||||
# Test chmod - should use openat, fchmodat, newfstatat
|
||||
if echo "$AVAILABLE_UTILS" | grep -q "chmod"; then
|
||||
cp -r test_dir test_chmod
|
||||
check_utility "chmod" "openat,fchmodat,newfstatat,chmod" "openat fchmodat" "-R 755 test_chmod" "recursive_chmod"
|
||||
fi
|
||||
|
||||
# Test chown - should use openat, fchownat, newfstatat
|
||||
if echo "$AVAILABLE_UTILS" | grep -q "chown"; then
|
||||
cp -r test_dir test_chown
|
||||
USER_ID=$(id -u)
|
||||
GROUP_ID=$(id -g)
|
||||
check_utility "chown" "openat,fchownat,newfstatat,chown,lchown" "openat fchownat" "-R $USER_ID:$GROUP_ID test_chown" "recursive_chown"
|
||||
fi
|
||||
|
||||
# Test chgrp - should use openat, fchownat, newfstatat
|
||||
if echo "$AVAILABLE_UTILS" | grep -q "chgrp"; then
|
||||
cp -r test_dir test_chgrp
|
||||
check_utility "chgrp" "openat,fchownat,newfstatat,chown,lchown" "openat fchownat" "-R $GROUP_ID test_chgrp" "recursive_chgrp"
|
||||
fi
|
||||
|
||||
# Test du - should use openat, newfstatat
|
||||
if echo "$AVAILABLE_UTILS" | grep -q "du"; then
|
||||
cp -r test_dir test_du
|
||||
check_utility "du" "openat,newfstatat,stat,lstat" "openat" "-a test_du" "directory_usage"
|
||||
fi
|
||||
|
||||
# Test mv - should use openat, renameat for directory moves
|
||||
if echo "$AVAILABLE_UTILS" | grep -q "mv"; then
|
||||
mkdir -p test_mv_src/sub
|
||||
echo "test" > test_mv_src/file.txt
|
||||
echo "test" > test_mv_src/sub/file2.txt
|
||||
check_utility "mv" "openat,renameat,newfstatat,rename" "openat" "test_mv_src test_mv_dst" "move_directory"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Basic safe traversal verification completed"
|
||||
echo ""
|
||||
echo "=== Additional Safety Checks ==="
|
||||
|
||||
# Check for dangerous patterns across all logs
|
||||
echo "Checking for dangerous path resolution patterns..."
|
||||
|
||||
# Check that we're not doing excessive path resolutions (sign of TOCTOU vulnerability)
|
||||
echo "Checking path resolution frequency..."
|
||||
for log in strace_*.log; do
|
||||
if [ -f "$log" ]; then
|
||||
path_resolutions=$(grep -c "test_" "$log" 2>/dev/null || echo "0")
|
||||
if [ "$path_resolutions" -gt 20 ]; then
|
||||
echo "⚠ $log: High path resolution count ($path_resolutions) - potential TOCTOU risk"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Summary ==="
|
||||
echo "All utilities are using safe traversal correctly!"
|
||||
Loading…
Add table
Add a link
Reference in a new issue