#!/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 . # # 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' 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 "$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."