Files
rspade_system/app/RSpade/CodeQuality/Rules/PHP/ProcOpenStreamTruncation_CodeQualityRule.php
2025-10-21 05:25:33 +00:00

171 lines
5.7 KiB
PHP
Executable File

<?php
namespace App\RSpade\CodeQuality\Rules\PHP;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class ProcOpenStreamTruncation_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'PHP-PROC-01';
}
public function get_name(): string
{
return 'proc_open() Usage Banned';
}
public function get_description(): string
{
return 'Bans proc_open() usage due to unfixable pipe buffer truncation bugs - use file redirection or shell_exec() instead';
}
public function get_file_patterns(): array
{
return ['*.php'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Check PHP file for ANY proc_open() usage
*
* BANNED: proc_open() is completely banned due to unfixable pipe buffer race conditions
* that cause silent data truncation on large outputs (35KB+).
*
* After 10+ attempts to fix this using various patterns (feof() loops, stream_set_blocking,
* etc.), we've determined proc_open() is fundamentally unreliable for our use cases.
*
* We never need asynchronous operations - all our use cases are synchronous command execution.
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor directories
if (str_contains($file_path, '/vendor/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments and strings removed
$sanitized_data = FileSanitizer::sanitize_php($contents);
$sanitized_code = $sanitized_data['content'];
// BLANKET BAN: Check if code contains ANY proc_open() usage
if (!preg_match('/\bproc_open\s*\(/i', $sanitized_code)) {
return; // No proc_open usage, all clear
}
// VIOLATION: Found proc_open() usage - this is banned
// Find the line number where proc_open appears
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
if (preg_match('/\bproc_open\s*\(/i', $sanitized_line)) {
$line_number = $line_num + 1;
$original_line = $original_lines[$line_num] ?? $sanitized_line;
$this->add_violation(
$file_path,
$line_number,
$this->get_violation_message(),
trim($original_line),
$this->get_resolution_message(),
'critical'
);
return; // Only report first occurrence
}
}
}
private function get_violation_message(): string
{
return "🚨 CRITICAL: proc_open() is BANNED in this codebase
After 10+ attempts to fix pipe buffer truncation bugs, proc_open() is now completely banned.
WHY THIS IS BANNED:
- Unfixable race conditions with feof() cause silent data loss on large outputs (35KB+)
- Even 'correct' patterns using while (!feof(\$pipes[...])) still have race conditions
- We never need asynchronous operations - all our use cases are synchronous
REAL-WORLD INCIDENTS:
1. JqhtmlWebpackCompiler.php - Compiled template truncated at 8KB, breaking JavaScript
2. Multiple attempts to fix with different buffering strategies all failed
3. Pattern matches known PHP bug reports going back years
THE FUNDAMENTAL PROBLEM:
- Child process writes to pipe
- Parent checks feof() but pipe hasn't been marked EOF yet
- fread() returns empty string
- Loop continues, feof() now returns true prematurely
- Remaining data silently lost
This is NOT a coding error - it's a race condition in proc_open() itself.";
}
private function get_resolution_message(): string
{
return "REQUIRED ACTION - Replace proc_open() with one of these RELIABLE alternatives:
OPTION 1: File Redirection (RECOMMENDED for large outputs)
// Redirect output to temp file - filesystem is reliable, pipes are not
\$temp_file = storage_path('rsx-tmp/output_' . uniqid() . '.txt');
\$command = sprintf(
'%s > %s 2>&1',
escapeshellarg(\$your_command),
escapeshellarg(\$temp_file)
);
exec(\$command, \$output_lines, \$return_code);
// Read complete output from file
if (file_exists(\$temp_file)) {
\$output = file_get_contents(\$temp_file);
unlink(\$temp_file); // Clean up
}
WHY THIS WORKS:
- OS guarantees file writes are complete before exec() returns
- No pipes, no race conditions, no truncation
- How webpack, rollup, and other build tools handle large outputs
- Simple, reliable, debuggable
OPTION 2: shell_exec() (for smaller outputs < 1MB)
\$output = shell_exec(\$command . ' 2>&1');
if (\$output === null) {
throw new RuntimeException('Command execution failed');
}
WHY THIS WORKS:
- PHP's shell_exec() uses different buffering mechanism
- No manual pipe handling, no feof() race conditions
- Specifically designed for capturing full command output
OPTION 3: exec() with output array (line-oriented output)
exec(\$command, \$output_lines, \$return_code);
\$output = implode(\"\\n\", \$output_lines);
WHY THIS WORKS:
- Reliable for line-oriented output
- No pipe buffer issues
- Simpler than proc_open()
DO NOT attempt to 'fix' proc_open() with better buffering strategies.
We've tried that 10+ times. It doesn't work. Use the alternatives above.";
}
}