$sanitized_line) { $line_number = $line_num + 1; // Skip if the line is empty in sanitized version (was a comment) if (trim($sanitized_line) === '') { continue; } // Check for exec( usage - word boundary ensures we don't match "execute(" etc. if (preg_match('/\bexec\s*\(/i', $sanitized_line)) { $original_line = $original_lines[$line_num] ?? $sanitized_line; $violation_message = "🚨 CRITICAL: exec() function detected - causes SILENT OUTPUT TRUNCATION exec() has a fundamental flaw: it reads command output LINE-BY-LINE into an array, which: - Hits memory/buffer limits on large outputs (>1MB typical) - Silently truncates output without throwing errors or exceptions - Causes catastrophic failures in compilation, bundling, and error reporting - Makes debugging impossible (you see partial output with no indication of truncation) Real-world example from this codebase: - jqhtml compilation of 220-line template was truncated at row 4 (mid-line) - No error thrown, no indication of failure - Took hours to diagnose because the truncation was SILENT - Fixed by replacing exec() with proc_open() - output jumped from 4KB to 35KB This is why exec() is BANNED across the entire application."; $resolution = "REQUIRED ACTION - Choose based on your needs: QUICKEST FIX (Drop-in replacement - no refactoring needed): Use \exec_safe() - RSpade framework helper with identical signature to exec(): \exec_safe(\$command, \$output, \$return_var); Simply replace exec() with \exec_safe(). That's it. No other code changes needed. Uses proc_open() internally to handle unlimited output without truncation. Example: // Before: exec('git status 2>&1', \$output, \$code); // After: \exec_safe('git status 2>&1', \$output, \$code); Benefits: - Zero refactoring - maintains exact same signature as exec() - All existing code continues to work identically - No silent truncation (uses proc_open() internally) - Framework helper function available everywhere FOR ADVANCED USERS (Need full control): Use proc_open() which streams unlimited output without size limits: \$descriptors = [ 0 => ['pipe', 'r'], // stdin 1 => ['pipe', 'w'], // stdout 2 => ['pipe', 'w'] // stderr ]; \$process = proc_open(\$command, \$descriptors, \$pipes); if (!is_resource(\$process)) { throw new \\RuntimeException(\"Failed to execute command\"); } fclose(\$pipes[0]); // Close stdin \$output_str = stream_get_contents(\$pipes[1]); // Read all stdout \$error_str = stream_get_contents(\$pipes[2]); // Read all stderr fclose(\$pipes[1]); fclose(\$pipes[2]); \$return_code = proc_close(\$process); // Combine stderr with stdout if command failed if (\$return_code !== 0 && !empty(\$error_str)) { \$output_str = \$error_str . \"\\n\" . \$output_str; } if (\$return_code !== 0) { throw new \\RuntimeException(\"Command failed: {\$output_str}\"); } Benefits: - Streams unlimited output (no size limits) - Separate stdout/stderr streams - Proper return code validation - No silent truncation FOR SIMPLE CASES (Don't need return code): Use shell_exec() for commands where you only need output: \$output = shell_exec(\$command); if (\$output === null) { throw new \\RuntimeException(\"Command failed\"); } Benefits: - Simple one-line replacement - Returns entire output as string (no line-by-line buffering) - No silent truncation - Drawback: Cannot get return code (assumes success if output is not null) RATIONALE: exec() was designed for simple command execution in the early PHP days. Modern PHP applications with large compilation outputs, bundling systems, and complex toolchains need proper stream handling. Both proc_open() and shell_exec() handle unlimited output correctly - exec() does not. NEVER use exec() for: - Compilation outputs (esbuild, webpack, babel, jqhtml) - Bundler commands - Any command with potentially large output (>100 lines) - Commands where you need to see complete error messages"; $this->add_violation( $file_path, $line_number, $violation_message, trim($original_line), $resolution, 'critical' ); } } } }