Description
Symfony version(s) affected: 4.4.8|5.1.2
- OS: Windows 10
- Server: IIS version 10
- PHP version 7.3
- Laravel version 6.18
Note: Bug originally encountered on Laravel 6.18 using symfony/process 4.4.8. I have replicated the issue on a fresh symfony/skeleton 5.1.2 base with the process package required through composer.
Description
When two or more sites are running IIS / FastCGI as different AppPoolIdentity users with limited group permissions to the sys_temp_dir, the first to write sf_proc_00.* files used by the Process class takes ownership, however any additional sites that attempt to write to this file will receive the following error:
A temporary file could not be opened to write the process output: fopen(C:\WINDOWS\TEMP\sf_proc_00.out.lock): failed to open stream: Permission denied
Our permissions are configured to prevent one site from modifying temporary files that are created by another.
How to reproduce
- In IIS: Add two sites using the Symfony\Process component with different Application Pools
- In IIS Application Pools: For both application pools used, select "Advanced Settings" and confirm the "Identity" field is set to the built-in "ApplicationPoolIdentity" account,
- In IIS Feature Panel: For both sites, select "Authentication" > "Anonymous Authentication", ensure it is enabled and click "Edit..." then select "Application Pool Identity" and click OK.
- Set limited permissions for the IIS_IUSRS group on the sys_temp_dir (this is C:\Windows\Temp on my machine). Verify the Users group does not exceed these permissions.
- Permissions used in testing: List folder / read data, Read attributes, Read extended attributes, Create files / write data, Read permissions
- For both sites, create some code that uses the Process class to run a command. (See Additional Context for the sample code I used to test)
- In Site A, run the code using the Process class and check the sys_temp_dir to confirm sf_proc_00 files were generated.
- In Site B, attempt to run the code using the Process class to throw the Permission Denied error.
Possible Solution
Perhaps a solution could be incrementing the filename when the lock files exist, but cannot be opened for writing, and removing the sf_proc_## files used in the WindowsPipes destructor.
I have limited experience in using the Process class, I'll post the code that has fixed it in my particular instance, however I do not know if it is ideal for all use cases.
symfony\process\Pipes\WindowsPipes.php, Line 58: (Try next iteration if lock file exists)
if (!$h = fopen($file.'.lock', 'w')) {
if (file_exists($file.'.lock'))
continue 2;
restore_error_handler();
throw new RuntimeException('A temporary file could not be opened to write the process output: '.$lastError);
}
symfony\process\Pipes\WindowsPipes.php, Line 88: (Remove files on destruct)
public function __destruct()
{
$this->close();
foreach ($this->files as $file) {
unlink($file);
unlink($file.'.lock');
}
}
Additional context
During replication, I made a simple controller that dumps output of the dir
command for testing:
src\Controller\HomeController.php:
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class HomeController
{
public function index()
{
$process = new Process(['dir']);
$process->run();
if(!$process->isSuccessful())
throw new ProcessFailedException($process);
return new Response($process->getOutput());
}
}
And attached it to the base route:
config\routes.yaml:
index:
path: /
controller: App\Controller\HomeController::index
The same code was used on both Site A and B.