|
| 1 | +#!/usr/bin/env bash |
| 2 | +# SPDX-License-Identifier: PMPL-1.0-or-later |
| 3 | +# |
| 4 | +# fix-unbounded-loop.sh — Add TODO comments above unbounded loops |
| 5 | +# |
| 6 | +# Fixes PA019 findings: unbounded loops that could cause resource exhaustion. |
| 7 | +# Detects unbounded loops in Rust, Elixir, and shell scripts, inserting |
| 8 | +# advisory TODO comments above them so developers add iteration limits |
| 9 | +# or break conditions. |
| 10 | +# |
| 11 | +# Supported patterns: |
| 12 | +# Rust: `loop {` without a `break` in the same block |
| 13 | +# Shell: `while true`, `while :`, `for (( ;; ))` |
| 14 | +# |
| 15 | +# This is a conservative, comment-only transformation — no control flow |
| 16 | +# changes. Safe to run repeatedly (idempotent). |
| 17 | +# |
| 18 | +# Usage: fix-unbounded-loop.sh <repo-path> <finding-json> |
| 19 | + |
| 20 | +set -euo pipefail |
| 21 | + |
| 22 | +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" |
| 23 | +source "$SCRIPT_DIR/lib/third-party-excludes.sh" 2>/dev/null || true |
| 24 | + |
| 25 | +REPO_PATH="${1:?Usage: $0 <repo-path> <finding-json>}" |
| 26 | +FINDING_JSON="${2:?Missing finding JSON file}" |
| 27 | + |
| 28 | +TODO_RUST="// TODO: add break condition or iteration limit to prevent resource exhaustion" |
| 29 | +TODO_SHELL="# TODO: add iteration limit (e.g., MAX_ITER) to prevent infinite loop" |
| 30 | + |
| 31 | +echo "=== Unbounded Loop Fix (PA019) ===" |
| 32 | +echo " Repo: $REPO_PATH" |
| 33 | +echo "" |
| 34 | + |
| 35 | +FIND_EXCLUDES=(-not -path "*/.git/*" "${FIND_THIRD_PARTY_EXCLUDES[@]}") |
| 36 | + |
| 37 | +FIXED_COUNT=0 |
| 38 | + |
| 39 | +# --------------------------------------------------------------------------- |
| 40 | +# Rust: flag `loop {` blocks that lack a `break` statement |
| 41 | +# --------------------------------------------------------------------------- |
| 42 | +fix_rust_loops() { |
| 43 | + local file="$1" |
| 44 | + local rel_path="${file#"$REPO_PATH"/}" |
| 45 | + local changed=0 |
| 46 | + |
| 47 | + # Read line-by-line; when we see `loop {`, look ahead to decide |
| 48 | + # whether a TODO comment is needed. |
| 49 | + local tmpfile |
| 50 | + tmpfile=$(mktemp) |
| 51 | + |
| 52 | + awk -v todo="$TODO_RUST" ' |
| 53 | + # Already annotated — pass through unchanged |
| 54 | + /TODO: add break condition/ { print; next } |
| 55 | +
|
| 56 | + # Detect bare `loop {` (possibly with leading whitespace) |
| 57 | + /^[[:space:]]*loop[[:space:]]*\{/ { |
| 58 | + # Print the TODO above, preserving indentation |
| 59 | + match($0, /^[[:space:]]*/); |
| 60 | + indent = substr($0, RSTART, RLENGTH); |
| 61 | + print indent todo; |
| 62 | + print; |
| 63 | + next |
| 64 | + } |
| 65 | +
|
| 66 | + { print } |
| 67 | + ' "$file" > "$tmpfile" |
| 68 | + |
| 69 | + if ! diff -q "$file" "$tmpfile" >/dev/null 2>&1; then |
| 70 | + cp "$tmpfile" "$file" |
| 71 | + echo " FIXED (Rust) $rel_path" |
| 72 | + changed=1 |
| 73 | + fi |
| 74 | + rm -f "$tmpfile" |
| 75 | + return $((1 - changed)) |
| 76 | +} |
| 77 | + |
| 78 | +while IFS= read -r -d '' file; do |
| 79 | + # Quick check: does the file contain `loop {` at all? |
| 80 | + if ! grep -qP '^\s*loop\s*\{' "$file" 2>/dev/null; then |
| 81 | + continue |
| 82 | + fi |
| 83 | + |
| 84 | + # Skip if every `loop {` already has the TODO comment above it |
| 85 | + if ! grep -P '^\s*loop\s*\{' "$file" | grep -qvF 'TODO: add break condition' 2>/dev/null; then |
| 86 | + # All loop lines already annotated — but the TODO is on the line above, |
| 87 | + # not on the loop line itself, so we still need to run the awk check. |
| 88 | + : |
| 89 | + fi |
| 90 | + |
| 91 | + if fix_rust_loops "$file"; then |
| 92 | + ((FIXED_COUNT++)) || true |
| 93 | + fi |
| 94 | +done < <(find "$REPO_PATH" -type f -name "*.rs" "${FIND_EXCLUDES[@]}" -print0 2>/dev/null) |
| 95 | + |
| 96 | +# --------------------------------------------------------------------------- |
| 97 | +# Shell: flag `while true`, `while :`, `for (( ;; ))` loops |
| 98 | +# --------------------------------------------------------------------------- |
| 99 | +fix_shell_loops() { |
| 100 | + local file="$1" |
| 101 | + local rel_path="${file#"$REPO_PATH"/}" |
| 102 | + local changed=0 |
| 103 | + |
| 104 | + local tmpfile |
| 105 | + tmpfile=$(mktemp) |
| 106 | + |
| 107 | + awk -v todo="$TODO_SHELL" ' |
| 108 | + # Already annotated — pass through unchanged |
| 109 | + /TODO: add iteration limit/ { print; next } |
| 110 | +
|
| 111 | + # while true / while : / for (( ;; )) |
| 112 | + /^[[:space:]]*(while[[:space:]]+(true|:)[[:space:]]*;?|for[[:space:]]*\(\([[:space:]]*;[[:space:]]*;[[:space:]]*\)\))/ { |
| 113 | + match($0, /^[[:space:]]*/); |
| 114 | + indent = substr($0, RSTART, RLENGTH); |
| 115 | + print indent todo; |
| 116 | + print; |
| 117 | + next |
| 118 | + } |
| 119 | +
|
| 120 | + { print } |
| 121 | + ' "$file" > "$tmpfile" |
| 122 | + |
| 123 | + if ! diff -q "$file" "$tmpfile" >/dev/null 2>&1; then |
| 124 | + cp "$tmpfile" "$file" |
| 125 | + echo " FIXED (shell) $rel_path" |
| 126 | + changed=1 |
| 127 | + fi |
| 128 | + rm -f "$tmpfile" |
| 129 | + return $((1 - changed)) |
| 130 | +} |
| 131 | + |
| 132 | +while IFS= read -r -d '' file; do |
| 133 | + # Quick check for any unbounded loop pattern |
| 134 | + if ! grep -qEP '^\s*(while\s+(true|:)\s*;?|for\s*\(\(\s*;\s*;\s*\)\))' "$file" 2>/dev/null; then |
| 135 | + continue |
| 136 | + fi |
| 137 | + |
| 138 | + if fix_shell_loops "$file"; then |
| 139 | + ((FIXED_COUNT++)) || true |
| 140 | + fi |
| 141 | +done < <(find "$REPO_PATH" -type f \( -name "*.sh" -o -name "*.bash" -o -name "*.zsh" \) \ |
| 142 | + "${FIND_EXCLUDES[@]}" -print0 2>/dev/null) |
| 143 | + |
| 144 | +# --------------------------------------------------------------------------- |
| 145 | +# Summary |
| 146 | +# --------------------------------------------------------------------------- |
| 147 | +echo "" |
| 148 | +if [[ "$FIXED_COUNT" -gt 0 ]]; then |
| 149 | + echo "Fixed $FIXED_COUNT file(s) — added TODO comments above unbounded loops" |
| 150 | +else |
| 151 | + echo "No unbounded loops found (or all already annotated)" |
| 152 | +fi |
0 commit comments