🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
171 lines
5.7 KiB
PHP
Executable File
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.";
|
|
}
|
|
}
|