fmf-tests/system-in-use/run-them-all/try-all-binaries-help-options.sh

332 lines
12 KiB
Bash
Executable file

#!/bin/bash
#
# Fuzzing Test for System Binaries
#
# Tests CLI programs by running them with random options and test files.
# Reports only real crashes (segfaults, memory errors).
# Safe: skips dangerous commands (rm, dd, mkfs, etc.)
#
# psklenar@redhat.com
# Copyright (C) 2026 Petr Sklenar
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# AI was used to generate this script.
set -uo pipefail
# Configuration
BIN_DIRS="/usr/bin /usr/sbin"
WORKSPACE="${HOME}/fuzz_lab"
LOG_FILE="${WORKSPACE}/segfault_fuzz_$(date +%Y%m%d_%H%M%S).log"
SUMMARY_FILE="${WORKSPACE}/segfault_summary.txt"
RUNS_PER_BIN=20
MAX_PARALLEL=10
TIMEOUT_SEC=5
KILL_AFTER=10
# Programs to skip (dangerous, interactive, GUI, or already reported)
SKIP_NAMES=(
bash rm dd mkfs reboot shutdown poweroff halt init telinit
sfdisk fdisk parted cfdisk gdisk sgdisk mkswap
login kill pkill killall systemd-ask-password sulogin chronyc chronyd
xfs_freeze fsck e2fsck xfs_repair
ip nmcli nmtui ifconfig route arp brctl bridge
iptables ip6tables nft ebtables arptables firewall-cmd
dhclient dhcpcd wpa_supplicant hostapd
dnf yum rpm zypper apt apt-get dpkg pacman
vi vim nvim nano emacs ed
gpg gpg2 gpg-agent ssh-keygen openssl
createuser dropuser createdb dropdb psql mysql mariadb mysqladmin mysql_secure_installation postgres pg_dump pg_restore mongosh redis-cli
passwd chpasswd useradd usermod userdel adduser deluser
aws gcloud kubectl docker podman
sql env_parallel parallel niceload parsort
unoconv ibus-setup ffmpeg ffprobe ffplay
gnome-keyring-3 gnome-boxes unix_chkpwd unix_update
cracklib-check code2color fsidd stund vimcolor
# Regex patterns for GUI programs
'.re:^gnome-|^gtk|^gdk|^gio-|^gsettings'
'.re:^kde|^plasma|^kwin|^dolphin|^konsole'
'.re:^xfce|^mate-|^cinnamon|^lxde|^lxqt'
'.re:^qt|^Qt|^wayland|^weston'
'.re:^X|^x11|^xorg|^xinit|^xterm'
'.re:^vlc|^totem|^rhythmbox|^brasero'
'.re:^firefox|^thunderbird|^chrome|^chromium'
'.re:^gedit|^kate|^kwrite|^pluma'
'.re:gui|^beakerlib'
'.re:^ip-|^nm-|^NetworkManager|^ovs-'
'.re:^[iI]'
'.re:^[hH]'
)
# Check if program should be skipped
should_skip() {
local name=$(basename "$1")
for skip in "${SKIP_NAMES[@]}"; do
if [[ "$skip" == .re:* ]]; then
[[ "$name" =~ ${skip#.re:} ]] && return 0
else
[[ "$name" == "$skip" ]] && return 0
fi
done
return 1
}
# Setup
echo "Setting up fuzzing test..."
mkdir -p "$WORKSPACE"
ulimit -n 65535 2>/dev/null || true
# Create test files
echo "Creating test files..."
touch "$WORKSPACE/empty.dat"
printf 'line1\nline2\n123\ntest\n' > "$WORKSPACE/small.txt"
cat > "$WORKSPACE/bad.json" <<'EOF'
{"key": "value", "broken": [1, 2, 3,
EOF
cat > "$WORKSPACE/bad.xml" <<'EOF'
<?xml version="1.0"?>
<root><unclosed><tag>value
EOF
dd if=/dev/urandom of="$WORKSPACE/random.bin" bs=1K count=10 2>/dev/null
printf '\x00\x00\xff\xff\xde\xad\xbe\xef\x00\x00' > "$WORKSPACE/nulls.bin"
dd if=/dev/zero of="$WORKSPACE/large.dat" bs=1M count=50 2>/dev/null
printf '\xc3\x28\xe2\x82\x28\xf0\x28\x8c\x28' > "$WORKSPACE/bad_utf8.txt"
printf '%%s%%s%%s%%n%%x%%x' > "$WORKSPACE/format.txt"
printf '../../../etc/passwd\n..\\..\\..\\windows\\system32' > "$WORKSPACE/paths.txt"
printf "' OR 1=1 --\n\"; DROP TABLE users; --" > "$WORKSPACE/sql_injection.txt"
printf '; ls -la\n| cat /etc/passwd\n`whoami`' > "$WORKSPACE/cmd.txt"
python3 -c "print('A' * 100000)" > "$WORKSPACE/longline.txt"
seq 1 100000 > "$WORKSPACE/manylines.txt"
cat "$WORKSPACE/small.txt" "$WORKSPACE/random.bin" > "$WORKSPACE/mixed.dat"
printf '\x1f\x8b\x08\x00\x00\x00\x00\x00' > "$WORKSPACE/gzip.dat"
printf '\x50\x4b\x03\x04' > "$WORKSPACE/zip.dat"
printf '\xff\xd8\xff\xe0\x00\x10JFIF' > "$WORKSPACE/fake.jpg"
printf '\x89PNG\r\n\x1a\n' > "$WORKSPACE/fake.png"
printf '\x7fELF\x02\x01\x01\x00' > "$WORKSPACE/fake.elf"
printf '#!/bin/sh\necho test' > "$WORKSPACE/script.sh"
printf -- '---\n-\n--help\n--version\n-v\n' > "$WORKSPACE/dashes.txt"
FUZZ_FILE_COUNT=22
FUZZ_FILE_NAMES="empty.dat small.txt bad.json bad.xml random.bin nulls.bin large.dat bad_utf8.txt format.txt paths.txt sql_injection.txt cmd.txt longline.txt manylines.txt mixed.dat gzip.dat zip.dat fake.jpg fake.png fake.elf script.sh dashes.txt"
# Tracking files
STOP_LOG="$WORKSPACE/script_stopped.txt"
CURRENT_RUN_FILE="$WORKSPACE/current_run.txt"
trap 'ec=$?; echo "$(date -Iseconds) EXIT code=$ec" >> "$STOP_LOG"' EXIT
trap 'echo "$(date -Iseconds) SIGNAL RECEIVED" >> "$STOP_LOG"; exit 129' HUP INT TERM
# Find programs to test
echo "Finding programs to test..."
ALL_BINS=()
while IFS= read -r bin; do
should_skip "$bin" && continue
ALL_BINS+=("$bin")
done < <(find $BIN_DIRS -maxdepth 1 -executable -type f 2>/dev/null | sort -u)
TOTAL=${#ALL_BINS[@]}
echo ""
echo "===================="
echo "Total programs to test: $TOTAL"
echo "Runs per program: $RUNS_PER_BIN"
echo "Max parallel jobs: $MAX_PARALLEL"
echo "Test files: $FUZZ_FILE_COUNT"
echo "Log file: $LOG_FILE"
echo "===================="
echo ""
> "$LOG_FILE"
# Fuzzing function - tests one program with random options and files
fuzz_binary() {
local bin="$1"
local name=$(basename "$bin")
local pkg=$(rpm -qf "$bin" --qf '%{NAME}-%{VERSION}-%{RELEASE}' 2>/dev/null || echo "unknown")
# Get command-line flags from --help
local flags=()
local tmp=$(mktemp)
timeout -k 5 3s "$bin" --help 2>&1 | tr -d '\0' | \
grep -aoE -e '--[a-zA-Z0-9][a-zA-Z0-9_-]*' -e '-[a-zA-Z0-9]' | \
grep -avE 'help|version|usage' | sort -u | head -n 50 > "$tmp"
while IFS= read -r f; do flags+=("$f"); done < "$tmp"
rm -f "$tmp"
[[ ${#flags[@]} -eq 0 ]] && flags=(-v -a -q -f -d -i -o)
# Run multiple test iterations
for ((r=0; r<RUNS_PER_BIN; r++)); do
# Pick 1-10 random flags (more options = better crash detection)
local n=$((1 + RANDOM % 10))
local chosen=$(printf '%s\n' "${flags[@]}" | shuf -n "$n" | tr '\n' ' ')
local args="$chosen"
local use_stdin=0
# Add test file if flags suggest it or randomly (33%)
local needs_file=0
[[ "$chosen" =~ (--file|--input|--config|--from|-f|-i|-c|--load|--read) ]] && needs_file=1
if [[ $needs_file -eq 1 ]] || [[ $((RANDOM % 3)) -eq 0 ]]; then
local farr=($FUZZ_FILE_NAMES)
local fidx=$((RANDOM % ${#farr[@]}))
local ffile="$WORKSPACE/${farr[$fidx]}"
if [[ $((RANDOM % 5)) -eq 0 ]]; then
use_stdin=1
args+=" -"
else
args+=" $ffile"
fi
fi
local err=$(mktemp)
local exit_code=0
echo "BIN: $bin | ARGS: $args | PKG: $pkg" > "$CURRENT_RUN_FILE"
# Run test with safety limits
(
ulimit -v 512000 2>/dev/null || true
ulimit -f 102400 2>/dev/null || true
export MALLOC_CHECK_=3 LC_ALL=C
unset DISPLAY WAYLAND_DISPLAY
if [[ $use_stdin -eq 1 ]]; then
local first="$WORKSPACE/$(echo $FUZZ_FILE_NAMES | awk '{print $1}')"
timeout -k $KILL_AFTER --foreground ${TIMEOUT_SEC}s bash -c \
"cat $first | $bin $args" >/dev/null 2>"$err"
else
timeout -k $KILL_AFTER --foreground ${TIMEOUT_SEC}s \
"$bin" $args >/dev/null 2>"$err"
fi
)
exit_code=$?
# Check for real crashes (exit codes: 139=SIGSEGV, 134=SIGABRT, etc.)
if [[ $exit_code -eq 139 ]] || [[ $exit_code -eq 134 ]] || [[ $exit_code -eq 136 ]] || \
[[ $exit_code -eq 132 ]] || [[ $exit_code -eq 133 ]] || [[ $exit_code -eq 135 ]] || \
grep -qiE 'segmentation fault|segfault|core dumped|double free|heap corruption|buffer overflow|stack smashing|memory corruption|use after free|ASAN|UBSAN' "$err" 2>/dev/null; then
# Log crash
{
echo "=================================================="
echo "REAL CRASH DETECTED"
echo "COMMAND: $bin $args"
echo "PACKAGE: $pkg"
echo "EXIT CODE: $exit_code"
case $exit_code in
139) echo "SIGNAL: SIGSEGV (Segmentation fault)" ;;
134) echo "SIGNAL: SIGABRT (Abort)" ;;
136) echo "SIGNAL: SIGFPE (Floating point exception)" ;;
132) echo "SIGNAL: SIGILL (Illegal instruction)" ;;
133) echo "SIGNAL: SIGTRAP (Trace trap)" ;;
135) echo "SIGNAL: SIGBUS (Bus error)" ;;
esac
echo "TIMESTAMP: $(date -Iseconds)"
echo "--------------------------------------------------"
echo "ERROR OUTPUT:"
head -n 30 "$err"
echo "=================================================="
echo ""
}
fi
rm -f "$err"
done
}
# Run tests in parallel
echo "Starting fuzzing tests..."
echo ""
running=0
for bin in "${ALL_BINS[@]}"; do
while [[ $running -ge $MAX_PARALLEL ]]; do
wait -n 2>/dev/null || true
((running--))
done
{ fuzz_binary "$bin" >> "$LOG_FILE" 2>&1; } &
((running++))
done
wait
echo ""
echo "Fuzzing complete!"
echo ""
# Generate summary
echo "Generating summary..."
crashes=$(grep -c "REAL CRASH DETECTED" "$LOG_FILE" 2>/dev/null || echo 0)
segfaults=$(grep -c "SIGSEGV" "$LOG_FILE" 2>/dev/null || echo 0)
aborts=$(grep -c "SIGABRT" "$LOG_FILE" 2>/dev/null || echo 0)
fpexc=$(grep -c "SIGFPE" "$LOG_FILE" 2>/dev/null || echo 0)
illins=$(grep -c "SIGILL" "$LOG_FILE" 2>/dev/null || echo 0)
buserr=$(grep -c "SIGBUS" "$LOG_FILE" 2>/dev/null || echo 0)
{
echo "SEGFAULT-FOCUSED FUZZ TEST SUMMARY - $(date)"
echo "============================================"
echo "Programs tested: $TOTAL"
echo "Runs per program: $RUNS_PER_BIN"
echo "Total test iterations: $((TOTAL * RUNS_PER_BIN))"
echo "Test files used: $FUZZ_FILE_COUNT"
echo ""
echo "REAL CRASHES FOUND:"
echo " Total: $crashes"
echo " Segmentation faults: $segfaults"
echo " Aborts (SIGABRT): $aborts"
echo " FP exceptions: $fpexc"
echo " Illegal instruction: $illins"
echo " Bus errors: $buserr"
echo ""
if [[ $crashes -gt 0 ]]; then
echo "AFFECTED PACKAGES:"
grep "PACKAGE:" "$LOG_FILE" | sed 's/.*PACKAGE: //' | sort | uniq -c | sort -rn
echo ""
echo "ALL CRASHES:"
grep "COMMAND:" "$LOG_FILE"
echo ""
fi
echo "Full log: $LOG_FILE"
} > "$SUMMARY_FILE"
# Collect coredumps
if [[ $crashes -gt 0 ]] && command -v coredumpctl &>/dev/null; then
echo "Collecting coredumps..."
COREDUMP_DIR="$WORKSPACE/coredumps_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$COREDUMP_DIR"
grep "COMMAND:" "$LOG_FILE" | sed 's/^.*COMMAND: //' | awk '{print $1}' | sort -u | while read -r cbin; do
[[ -z "$cbin" ]] && continue
cname=$(basename "$cbin")
out="$COREDUMP_DIR/coredump_${cname}.dump"
if coredumpctl dump "$cbin" -o "$out" 2>/dev/null; then
echo " Saved: $cname"
fi
done
echo "Coredumps saved to: $COREDUMP_DIR"
fi
# Show results
echo ""
echo "========================================"
cat "$SUMMARY_FILE"
echo "========================================"
echo ""
echo "Done! Check $SUMMARY_FILE for summary."