-
Notifications
You must be signed in to change notification settings - Fork 61
GH#2915: fix ShellCheck memory explosion from language server (11 GB RSS, kernel panics) #2918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # ShellCheck configuration for .agents/scripts/ | ||
| # | ||
| # GH#2915: The bash language server spawns ShellCheck with --external-sources | ||
| # on every edit. With 463 scripts cross-sourcing each other, this causes | ||
| # exponential AST expansion (observed: 11 GB RSS, kernel panics). | ||
| # | ||
| # LIMITATION: This .shellcheckrc is effective when ShellCheck is invoked with | ||
| # a file path (it walks up from the file's directory to find .shellcheckrc). | ||
| # However, the bash language server pipes content via stdin (-), so ShellCheck | ||
| # has no file path to walk from and CANNOT discover this file automatically. | ||
| # The primary fix is shellcheck-wrapper.sh (strips --external-sources from | ||
| # the language server's invocation). This .shellcheckrc is a secondary defense | ||
| # for direct ShellCheck invocations on files in this directory. | ||
| # | ||
| # Framework-invoked ShellCheck (linters-local.sh, pulse-wrapper.sh) already | ||
| # uses --norc and explicit flags, so this file doesn't affect them. | ||
|
|
||
| # Disable external source following — prevents recursive expansion | ||
| external-sources=false | ||
|
|
||
| # Inherit the same disable rules as root .shellcheckrc | ||
| # (ShellCheck only reads ONE .shellcheckrc — the first found — so we must | ||
| # duplicate the disables here rather than inheriting from root) | ||
| disable=SC2329 | ||
| disable=SC2317 | ||
| disable=SC2034 | ||
| disable=SC2001 | ||
| disable=SC2059 | ||
| disable=SC2012 | ||
| disable=SC2030 | ||
| disable=SC2031 | ||
| disable=SC2015 | ||
| disable=SC2129 | ||
| disable=SC2153 | ||
| disable=SC2004 | ||
| disable=SC2009 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,105 @@ | ||||||||||||||||||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||||||||||||||||||
| # Safe ShellCheck wrapper for language servers (shellcheck-wrapper.sh) | ||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||
| # The bash language server hardcodes --external-sources in every ShellCheck | ||||||||||||||||||||||||||||||||
| # invocation (bash-language-server/out/shellcheck/index.js:82). Combined with | ||||||||||||||||||||||||||||||||
| # --source-path pointing to a directory with 463+ cross-sourcing scripts, this | ||||||||||||||||||||||||||||||||
| # causes exponential AST expansion (observed: 11 GB RSS, kernel panics). | ||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||
| # This wrapper strips --external-sources from the arguments before passing them | ||||||||||||||||||||||||||||||||
| # to the real ShellCheck binary. It also enforces a memory limit via ulimit. | ||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||
| # Usage: | ||||||||||||||||||||||||||||||||
| # Set SHELLCHECK_PATH to this script's path, or place it earlier on PATH as | ||||||||||||||||||||||||||||||||
| # "shellcheck". The bash language server will use it instead of the real binary. | ||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||
| # Environment variables: | ||||||||||||||||||||||||||||||||
| # SHELLCHECK_REAL_PATH — Path to the real shellcheck binary (auto-detected) | ||||||||||||||||||||||||||||||||
| # SHELLCHECK_VMEM_MB — Virtual memory limit in MB (default: 2048) | ||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||
| # GH#2915: https://github.com/marcusquinn/aidevops/issues/2915 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| set -uo pipefail | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # --- Find the real ShellCheck binary --- | ||||||||||||||||||||||||||||||||
| _find_real_shellcheck() { | ||||||||||||||||||||||||||||||||
| local real_path="${SHELLCHECK_REAL_PATH:-}" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if [[ -n "$real_path" && -x "$real_path" ]]; then | ||||||||||||||||||||||||||||||||
| printf '%s' "$real_path" | ||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Search PATH, skipping this wrapper script | ||||||||||||||||||||||||||||||||
| local self | ||||||||||||||||||||||||||||||||
| self="$(realpath "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| local IFS=':' | ||||||||||||||||||||||||||||||||
|
Check warning on line 37 in .agents/scripts/shellcheck-wrapper.sh
|
||||||||||||||||||||||||||||||||
| local dir | ||||||||||||||||||||||||||||||||
| for dir in $PATH; do | ||||||||||||||||||||||||||||||||
| local candidate="${dir}/shellcheck" | ||||||||||||||||||||||||||||||||
| if [[ -x "$candidate" ]]; then | ||||||||||||||||||||||||||||||||
| local resolved | ||||||||||||||||||||||||||||||||
| resolved="$(realpath "$candidate" 2>/dev/null || readlink -f "$candidate" 2>/dev/null || echo "$candidate")" | ||||||||||||||||||||||||||||||||
| if [[ "$resolved" != "$self" ]]; then | ||||||||||||||||||||||||||||||||
| printf '%s' "$candidate" | ||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Common locations | ||||||||||||||||||||||||||||||||
| local loc | ||||||||||||||||||||||||||||||||
| for loc in /opt/homebrew/bin/shellcheck /usr/local/bin/shellcheck /usr/bin/shellcheck; do | ||||||||||||||||||||||||||||||||
| if [[ -x "$loc" ]]; then | ||||||||||||||||||||||||||||||||
| local resolved | ||||||||||||||||||||||||||||||||
| resolved="$(realpath "$loc" 2>/dev/null || readlink -f "$loc" 2>/dev/null || echo "$loc")" | ||||||||||||||||||||||||||||||||
| if [[ "$resolved" != "$self" ]]; then | ||||||||||||||||||||||||||||||||
| printf '%s' "$loc" | ||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| echo "shellcheck-wrapper: ERROR: cannot find real shellcheck binary" >&2 | ||||||||||||||||||||||||||||||||
| return 1 | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # --- Filter arguments --- | ||||||||||||||||||||||||||||||||
| _filter_args() { | ||||||||||||||||||||||||||||||||
| local args=() | ||||||||||||||||||||||||||||||||
| while [[ $# -gt 0 ]]; do | ||||||||||||||||||||||||||||||||
| case "$1" in | ||||||||||||||||||||||||||||||||
| --external-sources | -x) | ||||||||||||||||||||||||||||||||
| # Strip this flag — it causes unbounded source chain expansion | ||||||||||||||||||||||||||||||||
| ;; | ||||||||||||||||||||||||||||||||
| *) | ||||||||||||||||||||||||||||||||
| args+=("$1") | ||||||||||||||||||||||||||||||||
| ;; | ||||||||||||||||||||||||||||||||
| esac | ||||||||||||||||||||||||||||||||
| shift | ||||||||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||||||||
| printf '%s\n' "${args[@]}" | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # --- Main --- | ||||||||||||||||||||||||||||||||
| main() { | ||||||||||||||||||||||||||||||||
| local real_shellcheck | ||||||||||||||||||||||||||||||||
| real_shellcheck="$(_find_real_shellcheck)" || exit 1 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Read filtered args into array | ||||||||||||||||||||||||||||||||
| local filtered_args=() | ||||||||||||||||||||||||||||||||
| while IFS= read -r arg; do | ||||||||||||||||||||||||||||||||
| filtered_args+=("$arg") | ||||||||||||||||||||||||||||||||
| done < <(_filter_args "$@") | ||||||||||||||||||||||||||||||||
|
Comment on lines
+91
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The script uses
Suggested change
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Enforce memory limit (soft limit — ShellCheck can still be killed by the | ||||||||||||||||||||||||||||||||
| # memory pressure monitor if it exceeds this, but this prevents the worst case) | ||||||||||||||||||||||||||||||||
| local vmem_mb="${SHELLCHECK_VMEM_MB:-2048}" | ||||||||||||||||||||||||||||||||
| local vmem_kb=$((vmem_mb * 1024)) | ||||||||||||||||||||||||||||||||
| ulimit -v "$vmem_kb" 2>/dev/null || true | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| exec "$real_shellcheck" "${filtered_args[@]}" | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| main "$@" | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -458,6 +458,86 @@ add_local_bin_to_path() { | |||||||||||||||||
| return 0 | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| # GH#2915: Configure SHELLCHECK_PATH to use the safe wrapper that strips | ||||||||||||||||||
| # --external-sources. The bash language server hardcodes --external-sources | ||||||||||||||||||
| # in every ShellCheck invocation, causing exponential memory growth (11 GB+) | ||||||||||||||||||
| # when source chains span 463+ scripts. The wrapper intercepts this. | ||||||||||||||||||
| # | ||||||||||||||||||
| # Uses launchctl setenv (macOS) for GUI-launched apps + shell rc for terminals. | ||||||||||||||||||
| # This ensures all processes — regardless of shell — see the wrapper. | ||||||||||||||||||
| setup_shellcheck_wrapper() { | ||||||||||||||||||
| local wrapper_path="$HOME/.aidevops/agents/scripts/shellcheck-wrapper.sh" | ||||||||||||||||||
|
|
||||||||||||||||||
| # Verify the wrapper exists and is executable | ||||||||||||||||||
| if [[ ! -x "$wrapper_path" ]]; then | ||||||||||||||||||
| if [[ -f "$wrapper_path" ]]; then | ||||||||||||||||||
| chmod +x "$wrapper_path" | ||||||||||||||||||
| else | ||||||||||||||||||
| print_warning "ShellCheck wrapper not found at $wrapper_path (will be available after deploy)" | ||||||||||||||||||
| return 0 | ||||||||||||||||||
| fi | ||||||||||||||||||
| fi | ||||||||||||||||||
|
|
||||||||||||||||||
| # Verify the wrapper actually works (can find real shellcheck) | ||||||||||||||||||
| if ! "$wrapper_path" --version >/dev/null 2>&1; then | ||||||||||||||||||
| print_warning "ShellCheck wrapper cannot find real shellcheck binary — skipping" | ||||||||||||||||||
| return 0 | ||||||||||||||||||
| fi | ||||||||||||||||||
|
|
||||||||||||||||||
| local env_line | ||||||||||||||||||
| # shellcheck disable=SC2016 # env_line is written to rc files; must expand at shell startup | ||||||||||||||||||
| env_line='export SHELLCHECK_PATH="$HOME/.aidevops/agents/scripts/shellcheck-wrapper.sh"' | ||||||||||||||||||
| local added_to="" | ||||||||||||||||||
| local already_in="" | ||||||||||||||||||
|
|
||||||||||||||||||
| # Layer 1: launchctl setenv (macOS) — affects all GUI-launched processes | ||||||||||||||||||
| if [[ "$PLATFORM_MACOS" == "true" ]]; then | ||||||||||||||||||
| if launchctl setenv SHELLCHECK_PATH "$wrapper_path" 2>/dev/null; then | ||||||||||||||||||
| print_info "Set SHELLCHECK_PATH via launchctl (GUI processes)" | ||||||||||||||||||
| fi | ||||||||||||||||||
|
Comment on lines
+495
to
+497
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suppressing error output from
Suggested change
References
|
||||||||||||||||||
| fi | ||||||||||||||||||
|
|
||||||||||||||||||
| # Layer 2: Shell rc files — affects terminal sessions | ||||||||||||||||||
| local rc_file | ||||||||||||||||||
| while IFS= read -r rc_file; do | ||||||||||||||||||
| [[ -z "$rc_file" ]] && continue | ||||||||||||||||||
|
|
||||||||||||||||||
| if [[ ! -f "$rc_file" ]]; then | ||||||||||||||||||
| mkdir -p "$(dirname "$rc_file")" | ||||||||||||||||||
| touch "$rc_file" | ||||||||||||||||||
| fi | ||||||||||||||||||
|
|
||||||||||||||||||
| # Check if already added | ||||||||||||||||||
| if grep -q 'SHELLCHECK_PATH' "$rc_file" 2>/dev/null; then | ||||||||||||||||||
| already_in="${already_in:+$already_in, }$rc_file" | ||||||||||||||||||
| continue | ||||||||||||||||||
| fi | ||||||||||||||||||
|
|
||||||||||||||||||
| echo "" >>"$rc_file" | ||||||||||||||||||
| echo "# Added by aidevops setup (GH#2915: prevent ShellCheck memory explosion)" >>"$rc_file" | ||||||||||||||||||
| echo "$env_line" >>"$rc_file" | ||||||||||||||||||
| added_to="${added_to:+$added_to, }$rc_file" | ||||||||||||||||||
| done < <(get_all_shell_rcs) | ||||||||||||||||||
|
|
||||||||||||||||||
| if [[ -n "$added_to" ]]; then | ||||||||||||||||||
| print_success "Configured SHELLCHECK_PATH wrapper in: $added_to" | ||||||||||||||||||
| fi | ||||||||||||||||||
|
|
||||||||||||||||||
| if [[ -n "$already_in" ]]; then | ||||||||||||||||||
| print_info "SHELLCHECK_PATH already configured in: $already_in" | ||||||||||||||||||
| fi | ||||||||||||||||||
|
|
||||||||||||||||||
| if [[ -z "$added_to" && -z "$already_in" && "$PLATFORM_MACOS" != "true" ]]; then | ||||||||||||||||||
| print_warning "Could not configure SHELLCHECK_PATH automatically" | ||||||||||||||||||
| print_info "Add this to your shell config: $env_line" | ||||||||||||||||||
| fi | ||||||||||||||||||
|
|
||||||||||||||||||
| # Also export for current session | ||||||||||||||||||
| export SHELLCHECK_PATH="$wrapper_path" | ||||||||||||||||||
|
|
||||||||||||||||||
| return 0 | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| setup_aliases() { | ||||||||||||||||||
| print_info "Setting up shell aliases..." | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current method of iterating over
PATHusingfor dir in $PATHis not safe as it is subject to word splitting. This will cause issues if any directory in thePATHcontains spaces. Usingread -ato populate an array is a more robust approach that correctly handles such edge cases.