Skip to content

Fix STDIN consumption in alias groups for eval-file#86

Open
Copilot wants to merge 6 commits intomainfrom
copilot/fix-stdin-input-alias-group
Open

Fix STDIN consumption in alias groups for eval-file#86
Copilot wants to merge 6 commits intomainfrom
copilot/fix-stdin-input-alias-group

Conversation

Copy link
Contributor

Copilot AI commented Feb 15, 2026

Fix STDIN input with alias groups for eval-file command

  • Add static cache property to store STDIN contents
  • Modify execute_eval method to cache STDIN on first read
  • Use STDIN inode+device as cache key for cross-process sharing
  • Reuse cached STDIN for subsequent reads in alias groups
  • Add automatic cleanup of stale cache files (older than 1 hour)
  • Fallback to parent PID or environment variable when fstat unavailable
  • Pass all code quality checks (lint, phpcs, phpstan)
  • Fix test to verify STDIN executes on both sites in alias group
  • Fix cache sharing issue - all subprocesses now share the same cache file
Original prompt

This section details on the original issue you should resolve

<issue_title>Input from STDIN does not work with an alias group</issue_title>
<issue_description>cat cleanup_stuff.php | wp @site_a eval-file - works.
But it does not work if used with a alias group.
It only triggers on the first entry of the alias group.
eval-file alias

</issue_description>

Comments on the Issue (you are @copilot in this section)

@janw-me I also took alook at the output when adding the `--debug` flag. I'm willing to look into it but am uncertain where to start. @janw-me PS a dirty hack workaround I found out is misusing the regular `eval` command:

wp @blueprint eval "$(tail -n +2 cleanup.php )"</comment_new>
<comment_new>@schlessera
Oh, I assume STDIN is being read per site for the group, so after the first run, the empty STDIN is being read.

Not sure how we should go about for this, I'll have to do a bit of exploration here.</comment_new>


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 3 commits February 15, 2026 17:18
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix STDIN input handling for alias group Fix STDIN consumption in alias groups for eval-file Feb 15, 2026
Copilot AI requested a review from swissspidy February 15, 2026 17:29
@swissspidy

This comment was marked as resolved.

Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>

This comment was marked as resolved.

@swissspidy
Copy link
Member

@copilot Failed steps:

001 Scenario: Eval-file with STDIN should work with alias groups           # features/eval.feature:336
      Then STDOUT should match /Executed from STDIN.*Executed from STDIN/s # features/eval.feature:356
        Actual value: '@site1
        Executed from STDIN
        @site2
        ' (Exception)

Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>

This comment was marked as resolved.

@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 62.85714% with 13 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/EvalFile_Command.php 62.85% 13 Missing ⚠️

📢 Thoughts on this report? Let us know!

@swissspidy swissspidy marked this pull request as ready for review March 11, 2026 08:34
@swissspidy swissspidy requested a review from a team as a code owner March 11, 2026 08:34
Copilot AI review requested due to automatic review settings March 11, 2026 08:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses the long-standing issue where wp eval-file - only executes for the first site when run against an alias group, by ensuring STDIN can be reused across the multiple per-site subprocesses that WP-CLI spawns.

Changes:

  • Cache STDIN on first read and reuse it for subsequent executions when eval-file is invoked with - in alias groups.
  • Persist the cached STDIN to a temp file keyed off fstat(STDIN) (device+inode), with a cleanup routine for stale cache files.
  • Add a Behat scenario asserting STDIN-based eval-file runs on both sites in an alias group.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/EvalFile_Command.php Adds cross-process STDIN caching via a temp file and stale cache cleanup.
features/eval.feature Adds an acceptance test to verify STDIN eval-file works with alias groups.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +156 to +161
// Save to cache file for subsequent processes
$write_result = file_put_contents( self::$stdin_cache_file, self::$stdin_cache );
if ( false === $write_result ) {
WP_CLI::error( 'Failed to write STDIN cache file.' );
}

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing the STDIN cache with file_put_contents() without an explicit lock/atomic write can lead to partial reads if two processes hit this path concurrently (e.g. overlapping alias-group runs). Consider using LOCK_EX (or an atomic temp-file + rename) so readers never observe an incomplete cache file.

Suggested change
// Save to cache file for subsequent processes
$write_result = file_put_contents( self::$stdin_cache_file, self::$stdin_cache );
if ( false === $write_result ) {
WP_CLI::error( 'Failed to write STDIN cache file.' );
}
// Save to cache file for subsequent processes using an atomic write
$tmp_file = tempnam( sys_get_temp_dir(), 'wp-cli-eval-stdin-tmp-' );
if ( false === $tmp_file ) {
WP_CLI::error( 'Failed to create temporary STDIN cache file.' );
}
$write_result = file_put_contents( $tmp_file, self::$stdin_cache, LOCK_EX );
if ( false === $write_result ) {
// Best effort cleanup of temporary file
if ( file_exists( $tmp_file ) ) {
@unlink( $tmp_file );
}
WP_CLI::error( 'Failed to write STDIN cache file.' );
}
// Atomically replace (or create) the cache file so readers never see a partial write
if ( ! @rename( $tmp_file, self::$stdin_cache_file ) ) {
// Best effort cleanup of temporary file
if ( file_exists( $tmp_file ) ) {
@unlink( $tmp_file );
}
WP_CLI::error( 'Failed to finalize STDIN cache file.' );
}

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +170
// Check if cache file already exists (created by a previous subprocess)
if ( file_exists( self::$stdin_cache_file ) ) {
$stdin_contents = file_get_contents( self::$stdin_cache_file );
if ( false === $stdin_contents ) {
WP_CLI::error( 'Failed to read from STDIN cache file.' );
}
self::$stdin_cache = (string) $stdin_contents;

// Clean up old cache files (older than 1 hour) to prevent accumulation
self::cleanup_old_cache_files();
} else {
// First process: read from STDIN and cache it
$stdin_contents = file_get_contents( 'php://stdin' );
if ( false === $stdin_contents ) {
WP_CLI::error( 'Failed to read from STDIN.' );
}
self::$stdin_cache = (string) $stdin_contents;

// Save to cache file for subsequent processes
$write_result = file_put_contents( self::$stdin_cache_file, self::$stdin_cache );
if ( false === $write_result ) {
WP_CLI::error( 'Failed to write STDIN cache file.' );
}

// Clean up old cache files (older than 1 hour) to prevent accumulation
self::cleanup_old_cache_files();

// Note: We intentionally don't clean up the current cache file immediately.
// If we delete it in the shutdown function, subsequent child processes
// won't be able to read it. The cleanup_old_cache_files() method handles
// removal of stale cache files.
}
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The STDIN cache file path is deterministic and is trusted if it already exists. Because this file lives in the system temp dir, another local process could potentially pre-create/replace it (or make it a symlink), which would make this command read/eval unexpected content or overwrite an arbitrary target. Consider hardening by (a) verifying the cache path is a regular file (not a symlink), (b) refusing to use a pre-existing file unless it is owned by the current user and has safe permissions, and (c) creating/writing it with an exclusive lock and restrictive permissions (e.g. 0600) before later processes read it.

Suggested change
// Check if cache file already exists (created by a previous subprocess)
if ( file_exists( self::$stdin_cache_file ) ) {
$stdin_contents = file_get_contents( self::$stdin_cache_file );
if ( false === $stdin_contents ) {
WP_CLI::error( 'Failed to read from STDIN cache file.' );
}
self::$stdin_cache = (string) $stdin_contents;
// Clean up old cache files (older than 1 hour) to prevent accumulation
self::cleanup_old_cache_files();
} else {
// First process: read from STDIN and cache it
$stdin_contents = file_get_contents( 'php://stdin' );
if ( false === $stdin_contents ) {
WP_CLI::error( 'Failed to read from STDIN.' );
}
self::$stdin_cache = (string) $stdin_contents;
// Save to cache file for subsequent processes
$write_result = file_put_contents( self::$stdin_cache_file, self::$stdin_cache );
if ( false === $write_result ) {
WP_CLI::error( 'Failed to write STDIN cache file.' );
}
// Clean up old cache files (older than 1 hour) to prevent accumulation
self::cleanup_old_cache_files();
// Note: We intentionally don't clean up the current cache file immediately.
// If we delete it in the shutdown function, subsequent child processes
// won't be able to read it. The cleanup_old_cache_files() method handles
// removal of stale cache files.
}
}
// Check if cache file already exists (created by a previous subprocess).
if ( file_exists( self::$stdin_cache_file ) ) {
// Validate that the existing cache file is safe to use and read it.
$stdin_contents = self::read_safe_stdin_cache_file( self::$stdin_cache_file );
self::$stdin_cache = (string) $stdin_contents;
// Clean up old cache files (older than 1 hour) to prevent accumulation.
self::cleanup_old_cache_files();
} else {
// First process: read from STDIN and cache it.
$stdin_contents = file_get_contents( 'php://stdin' );
if ( false === $stdin_contents ) {
WP_CLI::error( 'Failed to read from STDIN.' );
}
self::$stdin_cache = (string) $stdin_contents;
// Save to cache file for subsequent processes, using an exclusive create
// and restrictive permissions to avoid symlink/pre-creation attacks.
self::write_safe_stdin_cache_file( self::$stdin_cache_file, self::$stdin_cache );
// Clean up old cache files (older than 1 hour) to prevent accumulation.
self::cleanup_old_cache_files();
// Note: We intentionally don't clean up the current cache file immediately.
// If we delete it in the shutdown function, subsequent child processes
// won't be able to read it. The cleanup_old_cache_files() method handles
// removal of stale cache files.
/**
* Safely read from an existing STDIN cache file.
*
* Validates that the cache file is a regular file (not a symlink) and, when
* possible, that it is owned by the current user and has safe permissions
* before reading its contents.
*
* @param string $cache_file Path to the cache file.
* @return string Cache file contents.
*/
private static function read_safe_stdin_cache_file( $cache_file ) {
// Refuse to follow symlinks.
if ( is_link( $cache_file ) || ! is_file( $cache_file ) ) {
WP_CLI::error( 'Insecure STDIN cache file detected (not a regular file).' );
}
// On POSIX systems, ensure the file is owned by the current user.
if ( function_exists( 'posix_geteuid' ) ) {
$file_owner = @fileowner( $cache_file );
$current_uid = @posix_geteuid();
if ( false !== $file_owner && false !== $current_uid && $file_owner !== $current_uid ) {
WP_CLI::error( 'Insecure STDIN cache file detected (unexpected owner).' );
}
}
// Ensure the file is not world- or group-writable. We allow read-only for
// group/others for backward compatibility, but reject writable bits.
$file_perms = @fileperms( $cache_file );
if ( false !== $file_perms ) {
$mode = $file_perms & 0777;
// Disallow group/other write bits.
if ( ( $mode & 0022 ) !== 0 ) {
WP_CLI::error( 'Insecure STDIN cache file detected (permissions too permissive).' );
}
}
$handle = @fopen( $cache_file, 'rb' );
if ( false === $handle ) {
WP_CLI::error( 'Failed to open STDIN cache file for reading.' );
}
// Acquire a shared lock while reading.
@flock( $handle, LOCK_SH );
$contents = stream_get_contents( $handle );
@flock( $handle, LOCK_UN );
fclose( $handle );
if ( false === $contents ) {
WP_CLI::error( 'Failed to read from STDIN cache file.' );
}
return (string) $contents;
}
/**
* Safely create and write the STDIN cache file.
*
* Uses an exclusive open and restrictive permissions to avoid symlink and
* pre-creation attacks in the system temporary directory.
*
* @param string $cache_file Path to the cache file.
* @param string $contents Contents to write.
* @return void
*/
private static function write_safe_stdin_cache_file( $cache_file, $contents ) {
// Attempt to create the file exclusively. This avoids clobbering an
// existing file that might have been created by another process.
$handle = @fopen( $cache_file, 'xb' );
if ( false === $handle ) {
// If the file suddenly exists, fall back to validating and reading it.
if ( file_exists( $cache_file ) ) {
// Another process likely created the cache file in the meantime.
return;
}
WP_CLI::error( 'Failed to create STDIN cache file.' );
}
// Restrict permissions to owner read/write where supported.
@chmod( $cache_file, 0600 );
// Acquire an exclusive lock while writing.
@flock( $handle, LOCK_EX );
$written = fwrite( $handle, $contents );
if ( false === $written ) {
@flock( $handle, LOCK_UN );
fclose( $handle );
WP_CLI::error( 'Failed to write STDIN cache file.' );
}
fflush( $handle );
@flock( $handle, LOCK_UN );
fclose( $handle );
}
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +161
// Save to cache file for subsequent processes
$write_result = file_put_contents( self::$stdin_cache_file, self::$stdin_cache );
if ( false === $write_result ) {
WP_CLI::error( 'Failed to write STDIN cache file.' );
}

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new STDIN caching logic persists the full STDIN contents to a temp file via file_put_contents( self::$stdin_cache_file, self::$stdin_cache ) without controlling the file permissions. On typical systems sys_get_temp_dir() points to a world-writable directory and the default permissions for file_put_contents() often make the file world-readable, so any other local user could read whatever is piped into eval-file - (potentially including database credentials or other secrets embedded in scripts). To mitigate this, ensure the cache file is created with restrictive permissions (e.g., using a secure temp-file creation mechanism or explicitly tightening mode/umask) so that only the invoking user can read it, and avoid leaving sensitive data readable under a predictable name in a shared temp directory.

Suggested change
// Save to cache file for subsequent processes
$write_result = file_put_contents( self::$stdin_cache_file, self::$stdin_cache );
if ( false === $write_result ) {
WP_CLI::error( 'Failed to write STDIN cache file.' );
}
// Save to cache file for subsequent processes with restrictive permissions.
// Temporarily tighten the umask so the created file is at most 0600, then restore it.
$old_umask = umask( 0177 );
$write_result = file_put_contents( self::$stdin_cache_file, self::$stdin_cache );
umask( $old_umask );
if ( false === $write_result ) {
WP_CLI::error( 'Failed to write STDIN cache file.' );
}
// Ensure the cache file has restrictive permissions (read/write for owner only).
if ( ! @chmod( self::$stdin_cache_file, 0600 ) ) {
WP_CLI::error( 'Failed to set secure permissions on STDIN cache file.' );
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Input from STDIN does not work with an alias group

3 participants