When two processes build an Mpdf against the same tempDir while its cache directory does not exist, both see it missing and both mkdir() it. The one that loses gets mkdir(): File exists, and Cache::createBasePath() returns false without looking again, so the constructor throws Temporary files directory "…/mpdf" is not writable for a directory that exists and is writable.
Gravity PDF hits this after its temp-directory cleanup removes tempDir/mpdf and two PDFs are then generated at the same moment (e.g. two form submissions, or a notification and a download). The thrown PDF is dropped from the email. Upstream has the same report open as mpdf#1775; mpdf#534 is the same warning with a permissions cause.
Where
src/Cache.php, createBasePath(): if (!file_exists($basePath)) { if (!$this->createDirectory($basePath)) { return false; } }.
src/Cache.php, createDirectory(): if (!mkdir($basePath, $permissions, true)) { return false; }.
src/ServiceFactory.php constructs two of these per Mpdf (tempDir/mpdf and tempDir/mpdf/ttfontdata), so either can lose.
Measured
PHP 8.5, src/Cache.php from origin/gravitypdf (identical to the copy Gravity PDF 6.17 ships). Each round deletes the directory, then starts four CLI processes that wait on a flag file and each run new Cache($base . '/mpdf'):
<?php
namespace Mpdf; class MpdfException extends \Exception {}
require __DIR__ . '/Cache.php';
[$base, $go] = [$argv[1], $argv[2]];
while (!file_exists($go)) { usleep(50); }
try { new Cache($base . '/mpdf'); echo "ok\n"; }
catch (MpdfException $e) { echo 'FAIL: ' . $e->getMessage() . "\n"; }
170 of 200 rounds had at least one process throw Temporary files directory ".../tmp/mpdf" is not writable, preceded by Warning: mkdir(): File exists in Cache.php on line 47.
Expected
A directory that another process created in the meantime counts as created. mkdir() failing is only a failure if the path is still not a directory afterwards, the usual race-safe form:
if (!mkdir($basePath, $permissions, true) && !is_dir($basePath)) {
return false;
}
The mkdir() warning should also not reach an error handler that converts warnings to exceptions (as in mpdf#1775's stack trace), so the call wants @ or an is_dir() re-check before the warning is raised. The umask chmod() loop that follows should only run for directories this process created.
When two processes build an
Mpdfagainst the sametempDirwhile its cache directory does not exist, both see it missing and bothmkdir()it. The one that loses getsmkdir(): File exists, andCache::createBasePath()returnsfalsewithout looking again, so the constructor throwsTemporary files directory "…/mpdf" is not writablefor a directory that exists and is writable.Gravity PDF hits this after its temp-directory cleanup removes
tempDir/mpdfand two PDFs are then generated at the same moment (e.g. two form submissions, or a notification and a download). The thrown PDF is dropped from the email. Upstream has the same report open as mpdf#1775; mpdf#534 is the same warning with a permissions cause.Where
src/Cache.php,createBasePath():if (!file_exists($basePath)) { if (!$this->createDirectory($basePath)) { return false; } }.src/Cache.php,createDirectory():if (!mkdir($basePath, $permissions, true)) { return false; }.src/ServiceFactory.phpconstructs two of these perMpdf(tempDir/mpdfandtempDir/mpdf/ttfontdata), so either can lose.Measured
PHP 8.5,
src/Cache.phpfromorigin/gravitypdf(identical to the copy Gravity PDF 6.17 ships). Each round deletes the directory, then starts four CLI processes that wait on a flag file and each runnew Cache($base . '/mpdf'):170 of 200 rounds had at least one process throw
Temporary files directory ".../tmp/mpdf" is not writable, preceded byWarning: mkdir(): File exists in Cache.php on line 47.Expected
A directory that another process created in the meantime counts as created.
mkdir()failing is only a failure if the path is still not a directory afterwards, the usual race-safe form:The
mkdir()warning should also not reach an error handler that converts warnings to exceptions (as in mpdf#1775's stack trace), so the call wants@or anis_dir()re-check before the warning is raised. The umaskchmod()loop that follows should only run for directories this process created.