Skip to content
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

[Process] Fix Inconsistent Exit Status in proc_get_status for PHP Versions Below 8.3 #53821

Merged
merged 1 commit into from Feb 9, 2024

Conversation

Luc45
Copy link
Contributor

@Luc45 Luc45 commented Feb 7, 2024

Q A
Branch? 5.4
Bug fix? yes
New feature? no
Deprecations? no
Issues N/A
License MIT

This PR addresses a bug in Symfony's Process component affecting PHP versions prior to 8.3. In these versions, calling proc_get_status multiple times on the same process resource only returns the correct exit status on the first call, with subsequent calls returning -1 due to the process being discarded. This behavior can lead to race conditions and incorrect process status reporting in Symfony applications.

PHP 8.3 changelog notes/fix PR:

Executing proc_get_status() multiple times

Executing proc_get_status() multiple times will now always return the right value on POSIX systems.
Previously, only the first call of the function returned the right value. Executing proc_close()
after proc_get_status() will now also return the right exit code. Previously this would return -1.

Symfony Process is very good at avoiding this, but it's possible to trigger this behavior if you pass the Process as a reference to the output callback itself, and inside of it you invoke a method that eventually triggers proc_get_status, such as isRunning, isSuccessful, etc.

This PR tries to mimick PHP 8.3 behavior in older versions, caching the first reported valid exit status code once the process exits, and reusing the cached value if the new exit status code is now invalid.

I believe this bug has been pestering Symfony Process for a long time, as per this Stack Overflow question, but it was very hard to track it down, and only happens in certain conditions, and it also have a racing condition factor depending on the time between the last output of the process and it's termination.

Changes:

  • Added caching mechanism for the exit status in the Process component.
  • Ensured the cached status is used for subsequent proc_get_status calls if PHP version is below 8.3.

Proof of Concept:

To demonstrate the issue and the effectiveness of the fix, a proof of concept script is included below. This script uses Symfony's Process component to start a subprocess that outputs a message and exits with a specific status code. The script then attempts to retrieve the exit status of the process using getExitCode(). In PHP versions prior to 8.3, without the proposed fix, this script will often incorrectly report an exit code of -1 due to the race condition described earlier.

<?php

if ( getenv( 'OUTPUT' ) ) {
	#echo 'This should fail when "wait" gets large';
	sleep( 2 );
	echo 'This should fail even when "wait" is small';
	die( 123 );
}

use Symfony\Component\Process\Process;

require_once __DIR__ . '/vendor/autoload.php';

$wait = 0;

do {
	// Increasingly bigger waits.
	$wait += 100000;
	echo sprintf( 'Wait: %s', $wait ) . PHP_EOL;

	$p = new Process( [ 'php', __FILE__ ], __DIR__, [
		'OUTPUT' => 1,
	] );

	$p->start( function ( string $type, string $out ) use ( $p ) {
		echo $out . PHP_EOL;

		/**
		 * Calling most methods in Symfony Process that triggers
		 * updateStatus() can potentially trigger the -1 bug.
		 *
		 * @see Process::updateStatus()
		 */
		echo sprintf( 'Is Running: %s', $p->isRunning() ? 'Yes' : 'No' ) . PHP_EOL;
		echo sprintf( 'Exit Code: %s', $p->getExitCode() ) . PHP_EOL;
	} );

	while ( $p->isRunning() ) {
		usleep( $wait );
	}

	if ( ! $p->isSuccessful() ) {
		break;
	}
} while ( true );

$is_started = $p->isStarted();
$is_running = $p->isRunning();
$exit_code  = $p->getExitCode();

echo sprintf( 'Started: %s, Running: %s, Exit code: %s', $is_started, $is_running, $exit_code ) . PHP_EOL;

Impact:

  • This change only affects Symfony applications running on PHP versions below 8.3.
  • It improves the reliability of process status reporting in these environments.
  • No breaking changes or backward compatibility issues are introduced.

Testing:

I haven't added tests to this PR because:

  • This bug involves racing conditions, which is hard to replicate.
  • It's past 1 AM local time right now, and I've been debugging this for the past 6 hours.
  • The provided "Proof of Concept" can serve as an initial check to confirm the bug.

I'm really not the type of developer that does not write tests, but I beg my pardon for the reasons above.

Considerations

  • Based on the proc_open implementation, this fix might not be needed on Windows.
  • You can find additional context for this bug in this Stack Overflow answer.

@carsonbot
Copy link

Hey!

Thanks for your PR. You are targeting branch "5.4" but it seems your PR description refers to branch "5.4, 6.4, 7.0".
Could you update the PR description or change target branch? This helps core maintainers a lot.

Cheers!

Carsonbot

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

Can you please add a test case to cover the behavior?

src/Symfony/Component/Process/Process.php Outdated Show resolved Hide resolved
@carsonbot carsonbot changed the title Fix Inconsistent Exit Status in proc_get_status for PHP Versions Below 8.3 [Process] Fix Inconsistent Exit Status in proc_get_status for PHP Versions Below 8.3 Feb 7, 2024
@nicolas-grekas
Copy link
Member

Reading now your full description... I'd still be great to figure out a test case :)

@stof
Copy link
Member

stof commented Feb 7, 2024

A first testcase could be one running your reproducer (even if it does not fail 100% of the time due to relying on a race condition, it would still be likely to fail sometimes if we reintroduce the issue in the future)

@Luc45
Copy link
Contributor Author

Luc45 commented Feb 7, 2024

I've added a test that reproduces the bug, it fails on 7.1 tag and passes on this branch.

@Luc45
Copy link
Contributor Author

Luc45 commented Feb 7, 2024

If I remove the fix, you can see the racing condition factor in the tests causing the tests to fail erratically on the longRunning process test.

image

@Luc45
Copy link
Contributor Author

Luc45 commented Feb 7, 2024

My tests uses sleep to mimick racing condition scenarios, which adds a total of 4 seconds to the Process test suite, which is not ideal. I've grouped them in time-sensitive, and I'm open to ideas. Maybe we could reduce the sleep from 2 to 1 second. [Done]

@Luc45
Copy link
Contributor Author

Luc45 commented Feb 7, 2024

I don't know why CliDumperTest::testCollapse is failing, as it doesn't seem to use Process at all.

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

LGTM thanks.

src/Symfony/Component/Process/Process.php Outdated Show resolved Hide resolved
src/Symfony/Component/Process/Tests/ProcessTest.php Outdated Show resolved Hide resolved
src/Symfony/Component/Process/Tests/ProcessTest.php Outdated Show resolved Hide resolved
@@ -1556,7 +1602,6 @@ public function testNotTerminableInputPipe()

/**
* @param string|array $commandline
* @param mixed $input
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't want to touch this but Fabbot code style tests asks to remove it. Bringing it up just for clarity.

@Luc45
Copy link
Contributor Author

Luc45 commented Feb 8, 2024

7.2 tests fail because of the short closure syntax, want me to keep it @nicolas-grekas?

@nicolas-grekas
Copy link
Member

nope, indeed

@Luc45
Copy link
Contributor Author

Luc45 commented Feb 8, 2024

Tests in Windows is failing because it doesn't have sleep. I've tweaked it to use inline PHP with sleep instead, and I can confirm it works by prepending time to the command, adjusting the sleep in the spawning process and re-running the test.

  • Sleep 5: php ./phpunit symfony src/Symfony/Component/Process 83,90s user 59,06s system 688% cpu 20,776 total
  • Sleep 1: php ./phpunit symfony src/Symfony/Component/Process 80,13s user 58,70s system 1094% cpu 12,684 total

Since it runs a bunch of tests in parallel, we have to ignore 80s and 59s because that's CPU time, and look at "actual time" which is the last one: 20s and 12s respectively, which sounds about right. Re-running.

@Luc45
Copy link
Contributor Author

Luc45 commented Feb 8, 2024

The remaining Windows failures seems unrelated

@nicolas-grekas
Copy link
Member

Thank you @Luc45.

@nicolas-grekas nicolas-grekas merged commit 3870d1e into symfony:5.4 Feb 9, 2024
10 of 11 checks passed
nicolas-grekas added a commit that referenced this pull request Feb 12, 2024
…-daubois)

This PR was merged into the 5.4 branch.

Discussion
----------

[Process] Fix failing tests causing segfaults

| Q             | A
| ------------- | ---
| Branch?       | 5.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | Fix tests
| License       | MIT

[This PR](#53821) introduces some code that [triggers segfaults](https://github.com/symfony/symfony/actions/runs/7870905388/job/21473073396) in newer branches (7.x especially). We can achieve the same goal by using some bash script which is lighter and avoid the segfaults. I first tried this fix on 7.x and solved the issue (that does not happen on older branches).

Commits
-------

041e178 [Process] Fix failing tests causing segfaults
This was referenced Feb 27, 2024
github-merge-queue bot pushed a commit to Lendable/composer-license-checker that referenced this pull request Mar 2, 2024
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [symfony/console](https://symfony.com)
([source](https://togithub.com/symfony/console)) | `6.4.4` -> `7.0.4` |
[![age](https://developer.mend.io/api/mc/badges/age/packagist/symfony%2fconsole/7.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/packagist/symfony%2fconsole/7.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/packagist/symfony%2fconsole/6.4.4/7.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/packagist/symfony%2fconsole/6.4.4/7.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [symfony/process](https://symfony.com)
([source](https://togithub.com/symfony/process)) | `6.4.4` -> `7.0.4` |
[![age](https://developer.mend.io/api/mc/badges/age/packagist/symfony%2fprocess/7.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/packagist/symfony%2fprocess/7.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/packagist/symfony%2fprocess/6.4.4/7.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/packagist/symfony%2fprocess/6.4.4/7.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>symfony/console (symfony/console)</summary>

### [`v7.0.4`](https://togithub.com/symfony/console/releases/tag/v7.0.4)

[Compare
Source](https://togithub.com/symfony/console/compare/v7.0.3...v7.0.4)

**Changelog**
(symfony/console@v7.0.3...v7.0.4)

- bug
[symfony/symfony#54009](https://togithub.com/symfony/symfony/issues/54009)
\[Console] Fix display of vertical Table on Windows OS
([@&#8203;VincentLanglet](https://togithub.com/VincentLanglet))
- bug
[symfony/symfony#54001](https://togithub.com/symfony/symfony/issues/54001)
\[Console] Fix display of Table on Windows OS
([@&#8203;VincentLanglet](https://togithub.com/VincentLanglet))
- bug
[symfony/symfony#53707](https://togithub.com/symfony/symfony/issues/53707)
\[Console] Fix color support for TTY output
([@&#8203;theofidry](https://togithub.com/theofidry))
- bug
[symfony/symfony#53711](https://togithub.com/symfony/symfony/issues/53711)
\[Console] Allow false as a $shortcut in InputOption
([@&#8203;jayminsilicon](https://togithub.com/jayminsilicon))

### [`v7.0.3`](https://togithub.com/symfony/console/releases/tag/v7.0.3)

[Compare
Source](https://togithub.com/symfony/console/compare/v7.0.2...v7.0.3)

**Changelog**
(symfony/console@v7.0.2...v7.0.3)

- bug
[symfony/symfony#53516](https://togithub.com/symfony/symfony/issues/53516)
\[Console] Allow '0' as a $shortcut in InputOption.php
([@&#8203;lawsonjl-ornl](https://togithub.com/lawsonjl-ornl))
- bug
[symfony/symfony#53576](https://togithub.com/symfony/symfony/issues/53576)
\[Console] Only execute additional checks for color support if the
output ([@&#8203;theofidry](https://togithub.com/theofidry))

### [`v7.0.2`](https://togithub.com/symfony/console/releases/tag/v7.0.2)

[Compare
Source](https://togithub.com/symfony/console/compare/v7.0.1...v7.0.2)

**Changelog**
(symfony/console@v7.0.1...v7.0.2)

- bug
[symfony/symfony#52940](https://togithub.com/symfony/symfony/issues/52940)
\[Console] Fix color support check on non-Windows platforms
([@&#8203;theofidry](https://togithub.com/theofidry))
- bug
[symfony/symfony#52941](https://togithub.com/symfony/symfony/issues/52941)
\[Console] Fix xterm detection
([@&#8203;theofidry](https://togithub.com/theofidry))

### [`v7.0.1`](https://togithub.com/symfony/console/releases/tag/v7.0.1)

[Compare
Source](https://togithub.com/symfony/console/compare/v7.0.0...v7.0.1)

**Changelog**
(symfony/console@v7.0.0...v7.0.1)

-   no significant changes

### [`v7.0.0`](https://togithub.com/symfony/console/releases/tag/v7.0.0)

[Compare
Source](https://togithub.com/symfony/console/compare/v6.4.4...v7.0.0)

**Changelog**
(symfony/console@v7.0.0-RC2...v7.0.0)

-   no significant changes

</details>

<details>
<summary>symfony/process (symfony/process)</summary>

### [`v7.0.4`](https://togithub.com/symfony/process/releases/tag/v7.0.4)

[Compare
Source](https://togithub.com/symfony/process/compare/v7.0.3...v7.0.4)

**Changelog**
(symfony/process@v7.0.3...v7.0.4)

- bug
[symfony/symfony#54006](https://togithub.com/symfony/symfony/issues/54006)
\[Process] Fix the `command -v` exception (@&#8203;kayw-geek)
- bug
[symfony/symfony#53821](https://togithub.com/symfony/symfony/issues/53821)
\[Process] Fix Inconsistent Exit Status in proc_get_status for PHP
Versions Below 8.3 ([@&#8203;Luc45](https://togithub.com/Luc45))

### [`v7.0.3`](https://togithub.com/symfony/process/releases/tag/v7.0.3)

[Compare
Source](https://togithub.com/symfony/process/compare/v7.0.2...v7.0.3)

**Changelog**
(symfony/process@v7.0.2...v7.0.3)

- bug
[symfony/symfony#53481](https://togithub.com/symfony/symfony/issues/53481)
\[Process] Fix executable finder when the command starts with a dash
([@&#8203;kayw-geek](https://togithub.com/kayw-geek))

### [`v7.0.2`](https://togithub.com/symfony/process/releases/tag/v7.0.2)

[Compare
Source](https://togithub.com/symfony/process/compare/v7.0.0...v7.0.2)

**Changelog**
(symfony/process@v7.0.1...v7.0.2)

- bug
[symfony/symfony#52864](https://togithub.com/symfony/symfony/issues/52864)
\[HttpClient]\[Mailer]\[Process] always pass microseconds to usleep as
integers ([@&#8203;xabbuh](https://togithub.com/xabbuh))

### [`v7.0.0`](https://togithub.com/symfony/process/releases/tag/v7.0.0)

[Compare
Source](https://togithub.com/symfony/process/compare/v6.4.4...v7.0.0)

**Changelog**
(symfony/process@v7.0.0-RC2...v7.0.0)

-   no significant changes

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/Lendable/composer-license-checker).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMjAuMiIsInVwZGF0ZWRJblZlciI6IjM3LjIyMC4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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.

None yet

4 participants